Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IntersectionObserver] #295 V2: visibility detection #523

Merged
merged 5 commits into from
Jun 27, 2024
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 131 additions & 42 deletions index.bs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ urlPrefix: https://html.spec.whatwg.org/multipage/
type: dfn; text: origin
urlPrefix: https://heycam.github.io/webidl/
url: #dfn-simple-exception; type:exception;
text: NotAllowedError
text: RangeError
text: TypeError
text: SyntaxError
Expand All @@ -64,12 +65,21 @@ urlPrefix: https://drafts.csswg.org/css-display/
url: #containing-block-chain; type: dfn; text: containing block chain
urlPrefix: http://www.w3.org/TR/css-masking-1/
url: #propdef-clip-path; type:dfn; text: clip-path
urlPrefix: https://drafts.csswg.org/css-overflow-3/
url: #ink-overflow-rectangle; type:dfn; text: ink overflow rectangle
url: #ink-overflow-region; type:dfn; text: ink overflow region
url: #overflow-properties; type:dfn; text: overflow properties
urlPrefix: https://drafts.csswg.org/css-transforms-1/
url: #transformation-matrix; type:dfn; text: transformation matrix
url: #serialization-of-the-computed-value; type:dfn; text: serialization
url: #identity-transform-function; type:dfn; text: identity transform function
url: #post-multiplied; type:dfn; text: post-multiplied
urlPrefix: https://drafts.csswg.org/cssom-view-1/
url: #pinch-zoom; type:dfn; text: pinch zoom
urlPrefix: https://drafts.csswg.org/css2/visuren.html
url: #viewport; type:dfn; text: viewport
urlPrefix: https://drafts.csswg.org/css-overflow-3/
url: #overflow-properties; type:dfn; text: overflow properties
urlPrefix: https://drafts.fxtf.org/filter-effects/
url: #funcdef-filter-blur; type:dfn; text: blur
</pre>

<pre class="link-defaults">
Expand Down Expand Up @@ -170,7 +180,7 @@ The IntersectionObserverCallback</h3>
callback IntersectionObserverCallback = undefined (sequence&lt;IntersectionObserverEntry> entries, IntersectionObserver observer);
</pre>

This callback will be invoked when there are changes to <a for="IntersectionObserver">target</a>'s
This callback will be invoked when there are changes to a <a for="IntersectionObserver">target</a>'s
intersection with the <a>intersection root</a>, as per the
<a>processing model</a>.

Expand All @@ -192,7 +202,7 @@ and it can observe any <a for="IntersectionObserver">target</a> {{Element}} that
{{IntersectionObserver/root}} in the <a>containing block chain</a>.
An {{IntersectionObserver}} with a <code>null</code> {{IntersectionObserver/root}}
is referred to as an <dfn for="IntersectionObserver">implicit root observer</dfn>.
Valid <a for="IntersectionObserver">targets</a> for an <a>implicit root observer</a> include
Valid <a for="IntersectionObserver">target</a>s for an <a>implicit root observer</a> include
any {{Element}} in the <a>top-level browsing context</a>,
as well as any {{Element}} in any <a>nested browsing context</a>
which is in the <a>list of the descendant browsing contexts</a> of the <a>top-level browsing context</a>.
Expand Down Expand Up @@ -225,6 +235,8 @@ interface IntersectionObserver {
readonly attribute DOMString rootMargin;
readonly attribute DOMString scrollMargin;
readonly attribute FrozenArray&lt;double&gt; thresholds;
readonly attribute long delay;
readonly attribute boolean trackVisibility;
undefined observe(Element target);
undefined unobserve(Element target);
undefined disconnect();
Expand All @@ -247,7 +259,7 @@ interface IntersectionObserver {

Note: {{MutationObserver}} does not implement {{unobserve()}}.
For {{IntersectionObserver}}, {{unobserve()}} addresses the
lazy-loading use case. After |target| becomes visible,
lazy-loading use case. After loading is initiated for |target|,
it does not need to be tracked.
It would be more work to either {{disconnect()}} all |target|s
and {{observe()}} the remaining ones,
Expand Down Expand Up @@ -306,6 +318,14 @@ interface IntersectionObserver {
If no |options|.{{IntersectionObserverInit/threshold}} was provided to the
{{IntersectionObserver}} constructor, or the sequence is empty, the value
of this attribute will be [0].
: <dfn>delay</dfn>
::
A number indicating the minimum delay in milliseconds
between notifications from this observer for a given target.
: <dfn>trackVisibility</dfn>
::
A boolean indicating whether this {{IntersectionObserver}} will track
changes in a target's <a>visibility</a>.
</div>

An {{Element}} is defined as having a <dfn for="IntersectionObserver">content clip</dfn> if its computed style has <a>overflow properties</a> that cause its content to be clipped to the element's <a>padding edge</a>.
Expand Down Expand Up @@ -401,6 +421,7 @@ interface IntersectionObserverEntry {
readonly attribute DOMRectReadOnly boundingClientRect;
readonly attribute DOMRectReadOnly intersectionRect;
readonly attribute boolean isIntersecting;
readonly attribute boolean isVisible;
readonly attribute double intersectionRatio;
readonly attribute Element target;
};
Expand All @@ -411,6 +432,7 @@ dictionary IntersectionObserverEntryInit {
required DOMRectInit boundingClientRect;
required DOMRectInit intersectionRect;
required boolean isIntersecting;
required boolean isVisible;
required double intersectionRatio;
required Element target;
};
Expand All @@ -428,8 +450,8 @@ dictionary IntersectionObserverEntryInit {
rects (up to but not including {{IntersectionObserver/root}}),
intersected with the <a>root intersection rectangle</a>.
This value represents the portion of
{{IntersectionObserverEntry/target}} actually visible
within the <a>root intersection rectangle</a>.
{{IntersectionObserverEntry/target}} that intersects with
the <a>root intersection rectangle</a>.
: <dfn>isIntersecting</dfn>
::
True if the {{IntersectionObserverEntry/target}} intersects with the
Expand All @@ -440,6 +462,11 @@ dictionary IntersectionObserverEntryInit {
to intersecting with a zero-area intersection rect (as will happen with
edge-adjacent intersections, or when the {{IntersectionObserverEntry/boundingClientRect}}
has zero area).
: <dfn>isVisible</dfn>
::
True if {{IntersectionObserver/trackVisibility}} is <code>true</code>
and the <a>visibility</a> algorithm,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implies the value false if trackVisibility is false, while later on the algorithm is specified to immediately returns true in that case. Of the two, false has a safer failure mode if someone thinks they're tracking visibility but they screwed up somewhere. Either way, the two parts of the spec need to agree.

Even better would be if isVisible were completely undefined if you had not enabled trackVisibility.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the purpose of feature detection, how about setting isVisible to null if trackVisibility is false? I think that's also more consistent with other IDL I have seen.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can't set a boolean to null, can you? Honest question -- I'm not sure what the WebIDL can express, nor what would be most consistent with similar APIs. This feels kind of off, but a permanent false when tracking isn't enabled wouldn't be terrible. The true specified in the algorithm below will lead to trouble.

Any thoughts, @annevk or @zcorpan?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not an IDL expert, but for the record chromium's IDL code generator allows null-able booleans. I wrote a chromium patch which would implement the behavior I proposed, and it works.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can have nullable booleans (the IDL of this PR does not do that btw), but generally it would be better to use an enum.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure an enum will work here, because we need two different values that will evaluate to false when cast to boolean.

If there are no objections, I'll just make it a permanent false when tracking isn't enabled. I think that's probably what we want from a developer ergonomics no-surprises point of view. Feature detection can always be done via IntersectionObserverEntry.prototype.hasOwnProperty('isVisible').

when run on {{IntersectionObserverEntry/target}}, returns <code>true</code>.
: <dfn>intersectionRatio</dfn>
::
If the {{IntersectionObserverEntry/boundingClientRect}} has non-zero area,
Expand Down Expand Up @@ -474,6 +501,8 @@ dictionary IntersectionObserverInit {
DOMString rootMargin = "0px";
DOMString scrollMargin = "0px";
(double or sequence&lt;double>) threshold = 0;
long delay = 0;
boolean trackVisibility = false;
};
</pre>

Expand Down Expand Up @@ -513,6 +542,16 @@ dictionary IntersectionObserverInit {
by <a>getting the bounding box</a> for <a for="IntersectionObserver">target</a>.

Note: 0.0 is effectively "any non-zero number of pixels".
: <dfn>delay</dfn>
::
A number specifying the minimum delay in milliseconds
between notifications from the observer for a given target.
: <dfn>trackVisibility</dfn>
::
A boolean indicating whether the observer should track <a>visibility</a>.
Note that tracking <a>visibility</a> is likely to be a more expensive operation
than tracking intersections. It is recommended that this option be used
only when necessary.
</div>

<h2 dfn id='intersection-observer-processing-model'>
Expand All @@ -538,23 +577,32 @@ Element</h4>
<dfn attribute for=Element>\[[RegisteredIntersectionObservers]]</dfn> slot,
which is initialized to an empty list.
This list holds <dfn interface>IntersectionObserverRegistration</dfn> records,
which have an <dfn attribute for=IntersectionObserverRegistration>observer</dfn> property
holding an {{IntersectionObserver}}, a <dfn attribute for=IntersectionObserverRegistration>previousThresholdIndex</dfn> property
holding a number between -1 and the length of the observer's {{IntersectionObserver/thresholds}} property (inclusive), and
a <dfn attribute for=IntersectionObserverRegistration>previousIsIntersecting</dfn> property holding a boolean.
which have:
* an <dfn attribute for=IntersectionObserverRegistration>observer</dfn> property
holding an {{IntersectionObserver}}.
* a <dfn attribute for=IntersectionObserverRegistration>previousThresholdIndex</dfn> property
holding a number between -1 and the length of the observer's {{IntersectionObserver/thresholds}} property (inclusive).
* a <dfn attribute for=IntersectionObserverRegistration>previousIsIntersecting</dfn> property
holding a boolean.
* a <dfn attribute for=IntersectionObserverRegistration>lastUpdateTime</dfn> property
holding a {{DOMHighResTimeStamp}} value.
* a <dfn attribute for=IntersectionObserverRegistration>previousIsVisible</dfn> property
holding a boolean.

<h4 id='intersection-observer-private-slots'>
IntersectionObserver</h4>

{{IntersectionObserver}} objects have internal
<dfn attribute for=IntersectionObserver>\[[QueuedEntries]]</dfn> and
<dfn attribute for=IntersectionObserver>\[[ObservationTargets]]</dfn> slots,
which are initialized to empty lists and an internal
<dfn attribute for=IntersectionObserver>\[[callback]]</dfn> slot
which is initialized by {{IntersectionObserver(callback, options)}}</a>.
They also have internal <dfn attribute for=IntersectionObserver>\[[rootMargin]]</dfn>
and <dfn attribute for=IntersectionObserver>\[[scrollMargin]]</dfn> slots
which are lists of four pixel lengths or percentages.
{{IntersectionObserver}} objects have the following internal slots:
* A <dfn attribute for=IntersectionObserver>\[[QueuedEntries]]</dfn> slot
initialized to an empty list.
* A <dfn attribute for=IntersectionObserver>\[[ObservationTargets]]</dfn> slot
initialized to an empty list.
* A <dfn attribute for=IntersectionObserver>\[[callback]]</dfn> slot
which is initialized by {{IntersectionObserver(callback, options)}}.
* A <dfn attribute for=IntersectionObserver>\[[rootMargin]]</dfn> slot
which is a list of four pixel lengths or percentages.
* A <dfn attribute for=IntersectionObserver>\[[scrollMargin]]</dfn> slot
which is a list of four pixel lengths or percentages.

<h3 id='algorithms'>
Algorithms</h2>
Expand Down Expand Up @@ -584,7 +632,12 @@ and an {{IntersectionObserverInit}} dictionary |options|, run these steps:
8. If |thresholds| is empty, append <code>0</code> to |thresholds|.
9. The {{IntersectionObserver/thresholds}} attribute getter will return
this sorted |thresholds| list.
10. Return |this|.
10. If |options|.{{IntersectionObserverInit/trackVisibility}} is <code>true</code>,
and |options|.{{IntersectionObserverInit/delay}} is not specified or is less than <code>100</code>,
<a>throw</a> a {{NotAllowedError}} exception.
szager-chromium marked this conversation as resolved.
Show resolved Hide resolved
11. Set |this|'s internal {{[[delay]]}} slot to |options|.{{IntersectionObserverInit/delay}}.
szager-chromium marked this conversation as resolved.
Show resolved Hide resolved
12. Set |this|'s internal {{[[trackVisibility]]}} slot to |options|.{{IntersectionObserverInit/trackVisibility}}.
13. Return |this|.

<h4 id='observe-target-element'>Observe a target Element</h4>

Expand All @@ -597,7 +650,8 @@ and an {{Element}} |target|, follow these steps:
an {{IntersectionObserverRegistration}} record
with an {{IntersectionObserverRegistration/observer}} property set to |observer|,
a {{IntersectionObserverRegistration/previousThresholdIndex}} property set to <code>-1</code>,
and a {{IntersectionObserverRegistration/previousIsIntersecting}} property set to <code>false</code>.
a {{IntersectionObserverRegistration/previousIsIntersecting}} property set to <code>false</code>,
and a {{IntersectionObserverRegistration/previousIsVisible}} property set to <code>false</code>.
3. Append |intersectionObserverRegistration|
to |target|'s internal {{[[RegisteredIntersectionObservers]]}} slot.
4. Add |target| to |observer|'s internal {{[[ObservationTargets]]}} slot.
Expand Down Expand Up @@ -691,6 +745,32 @@ run these steps:
6. Map |intersectionRect| to the coordinate space of the <a>viewport</a> of the {{document}} containing |target|.
7. Return |intersectionRect|.

<h4 id='calculate-visibility-algo'>
Compute whether a Target is unoccluded, untransformed, unfiltered, and opaque.</h4>

To compute the <dfn>visibility</dfn> of a <a for="IntersectionObserver">target</a>, run these steps:
1. If the |observer|'s {{IntersectionObserver/trackVisibility}} attribute is false, return true.
szager-chromium marked this conversation as resolved.
Show resolved Hide resolved
2. If the <a for="IntersectionObserver">target</a> has an <a>effective transformation matrix</a> other than a 2D translation or proportional 2D upscaling, return false.
3. If the <a for="IntersectionObserver">target</a>, or any element in its <a>containing block chain</a>, has an effective opacity other than 100%, return false.
4. If the <a for="IntersectionObserver">target</a>, or any element in its <a>containing block chain</a>, has any filters applied, return false.
5. If the implementation cannot guarantee that the <a for="IntersectionObserver">target</a> is completely unoccluded by other page content, return false.

Note: Implementations should use the <a>ink overflow rectangle</a> of page content when determining whether a <a for="IntersectionObserver">target</a> is occluded. For blur effects, which have theoretically infinite extent, the <a>ink overflow rectangle</a> is defined by the finite-area approximation described for the <a>blur</a> filter function.

6. Return true.

<h4 id='calculate-effective-transformation-matrix'>Calculate a <a for="IntersectionObserver">target</a>'s Effective Transformation Matrix</h4>
To compute the <dfn>effective transformation matrix</dfn> of a <a for="IntersectionObserver">target</a>, run these steps:
1. Let |matrix| be the <a>serialization</a> of the <a>identity transform function</a>.
2. Let |container| be the target.
3. While |container| is not the <a>intersection root</a>:
1. Set |t| to |container|'s <a>transformation matrix</a>.
2. Set |matrix| to |t| <a>post-multiplied</a> by |matrix|.
3. If |container| is the root element of a <a>nested browsing context</a>,
update |container| to be the <a>browsing context container</a> of |container|. Otherwise, update |container| to be the <a>containing block</a> of |container|.
4. Return |matrix|.


<h4 id='update-intersection-observations-algo'>
Run the Update Intersection Observations Steps</h4>

Expand All @@ -703,45 +783,54 @@ To <dfn export>run the update intersection observations steps</dfn> for a
2. For each |observer| in |observer list|:
1. Let |rootBounds| be |observer|'s <a>root intersection rectangle</a>.
2. For each |target| in |observer|'s internal {{[[ObservationTargets]]}} slot, processed in the same order that {{observe()}} was called on each |target|:
1. Let:
1. Let |registration| be the {{IntersectionObserverRegistration}} record
in |target|'s internal {{[[RegisteredIntersectionObservers]]}} slot
whose {{IntersectionObserverRegistration/observer}} property is equal to |observer|.
2. If <code>(|time| - |registration|.{{IntersectionObserverRegistration/lastUpdateTime}} < |observer|.{{IntersectionObserver/delay}})</code>, skip further processing for |target|.
3. Set |registration|.{{IntersectionObserverRegistration/lastUpdateTime}} to |time|.
4. Let:
- |thresholdIndex| be 0.
- |isIntersecting| be false.
- |targetRect| be a {{DOMRectReadOnly}} with |x|, |y|, |width|, and |height| set to 0.
- |intersectionRect| be a {{DOMRectReadOnly}} with |x|, |y|, |width|, and |height| set to 0.
2. If the <a>intersection root</a> is not the <a>implicit root</a>,
5. If the <a>intersection root</a> is not the <a>implicit root</a>,
and |target| is not in the same {{document}} as the <a>intersection root</a>,
skip to step 11.
3. If the <a>intersection root</a> is an {{Element}},
6. If the <a>intersection root</a> is an {{Element}},
and |target| is not a descendant of the <a>intersection root</a>
in the <a>containing block chain</a>, skip to step 11.
4. Set |targetRect| to the {{DOMRectReadOnly}} obtained by <a>getting the bounding box</a> for
7. Set |targetRect| to the {{DOMRectReadOnly}} obtained by <a>getting the bounding box</a> for
|target|.
4. Let |intersectionRect| be the result of running the <a>compute the intersection</a>
8. Let |intersectionRect| be the result of running the <a>compute the intersection</a>
algorithm on |target| and |observer|'s <a>intersection root</a>.
5. Let |targetArea| be |targetRect|'s area.
6. Let |intersectionArea| be |intersectionRect|'s area.
7. Let |isIntersecting| be true if |targetRect| and |rootBounds| intersect or are edge-adjacent,
9. Let |targetArea| be |targetRect|'s area.
10. Let |intersectionArea| be |intersectionRect|'s area.
11. Let |isIntersecting| be true if |targetRect| and |rootBounds| intersect or are edge-adjacent,
even if the intersection has zero area (because |rootBounds| or |targetRect| have
zero area).
9. If |targetArea| is non-zero, let |intersectionRatio| be |intersectionArea| divided by |targetArea|.<br>
12. If |targetArea| is non-zero, let |intersectionRatio| be |intersectionArea| divided by |targetArea|.<br>
Otherwise, let |intersectionRatio| be <code>1</code> if |isIntersecting| is true, or <code>0</code> if |isIntersecting| is false.
10. Set |thresholdIndex| to the index of the first entry in |observer|.{{thresholds}} whose value is greater than |intersectionRatio|, or the length of |observer|.{{thresholds}} if |intersectionRatio| is greater than or equal to the last entry in |observer|.{{thresholds}}.
11. Let |intersectionObserverRegistration| be the {{IntersectionObserverRegistration}} record
in |target|'s internal {{[[RegisteredIntersectionObservers]]}} slot
whose {{IntersectionObserverRegistration/observer}} property is equal to |observer|.
12. Let |previousThresholdIndex| be the |intersectionObserverRegistration|'s
13. Set |thresholdIndex| to the index of the first entry in |observer|.{{thresholds}} whose value is greater than |intersectionRatio|, or the length of |observer|.{{thresholds}} if |intersectionRatio| is greater than or equal to the last entry in |observer|.{{thresholds}}.
14. Let |isVisible| be the result of running the <a>visibility</a> algorithm on |target|.
15. Let |previousThresholdIndex| be the |registration|'s
{{IntersectionObserverRegistration/previousThresholdIndex}} property.
13. Let |previousIsIntersecting| be the |intersectionObserverRegistration|'s
16. Let |previousIsIntersecting| be the |registration|'s
{{IntersectionObserverRegistration/previousIsIntersecting}} property.
14. If |thresholdIndex| does not equal |previousThresholdIndex| or if
|isIntersecting| does not equal |previousIsIntersecting|,
17. Let |previousIsVisible| be the |registration|'s
{{IntersectionObserverRegistration/previousIsVisible}} property.
18. If |thresholdIndex| does not equal |previousThresholdIndex|,
or if |isIntersecting| does not equal |previousIsIntersecting|,
or if |isVisible| does not equal |previousIsVisible|,
<a>queue an IntersectionObserverEntry</a>,
passing in |observer|, |time|, |rootBounds|,
|targetRect|, |intersectionRect|, |isIntersecting|, and |target|.
15. Assign |thresholdIndex| to |intersectionObserverRegistration|'s
|targetRect|, |intersectionRect|, |isIntersecting|,
|isVisible|, and |target|.
19. Assign |thresholdIndex| to |registration|'s
{{IntersectionObserverRegistration/previousThresholdIndex}} property.
16. Assign |isIntersecting| to |intersectionObserverRegistration|'s
20. Assign |isIntersecting| to |registration|'s
{{IntersectionObserverRegistration/previousIsIntersecting}} property.
21. Assign |isVisible| to |registration|'s
{{IntersectionObserverRegistration/previousIsVisible}} property.

<h3 id='lifetime'>
IntersectionObserver Lifetime</h2>
Expand Down
Loading