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 all 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
177 changes: 135 additions & 42 deletions index.bs
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,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 +179,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 +201,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 +234,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 +258,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 +317,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 +420,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 +431,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 +449,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 +461,10 @@ 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>
::
Contains the result of running the <a>visibility</a> algorithm
on {{IntersectionObserverEntry/target}}.
: <dfn>intersectionRatio</dfn>
::
If the {{IntersectionObserverEntry/boundingClientRect}} has non-zero area,
Expand Down Expand Up @@ -474,6 +499,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 +540,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 +575,38 @@ 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.
* A <dfn attribute for=IntersectionObserver>\[[thresholds]]</dfn> slot
which is initialized by {{IntersectionObserver(callback, options)}}.
* A <dfn attribute for=IntersectionObserver>\[[delay]]</dfn> slot
which is initialized by {{IntersectionObserver(callback, options)}}.
* A <dfn attribute for=IntersectionObserver>\[[trackVisibility]]</dfn> slot
which is initialized by {{IntersectionObserver(callback, options)}}.

<h3 id='algorithms'>
Algorithms</h2>
Expand Down Expand Up @@ -584,7 +636,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. Let |delay| be the value of |options|.{{IntersectionObserverInit/delay}}.
11. If |options|.{{IntersectionObserverInit/trackVisibility}} is true
and |delay| is less than <code>100</code>, set |delay| to <code>100</code>.
11. Set |this|'s internal {{[[delay]]}} slot to |options|.{{IntersectionObserverInit/delay}} to |delay|.
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 +654,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 false,
and a {{IntersectionObserverRegistration/previousIsVisible}} property set to false.
3. Append |intersectionObserverRegistration|
to |target|'s internal {{[[RegisteredIntersectionObservers]]}} slot.
4. Add |target| to |observer|'s internal {{[[ObservationTargets]]}} slot.
Expand Down Expand Up @@ -691,6 +749,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 false.
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 +787,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