Skip to content

Commit cf52db0

Browse files
authored
feat: widen element types to Element and accept undefined refs (#156)
- Observe any Element (SVG included), matching the native API; root now accepts Element | Document | null as the platform does. - Accept undefined alongside null for element/elements and in the Multiple map key types: bind:this types as `T | undefined` in Svelte 5, so every consumer hit this mismatch. Runtime guards normalized to treat null/undefined uniformly.
1 parent c3944d6 commit cf52db0

13 files changed

Lines changed: 212 additions & 44 deletions

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,10 @@ In this example, "Hello world" fades in when its containing element intersects t
130130

131131
| Name | Description | Type | Default value |
132132
| :----------- | :---------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------ | :------------ |
133-
| element | Observed element | `null` or `HTMLElement` | `null` |
133+
| element | Observed element | `null` \| `undefined` \| `Element` | `null` |
134134
| once | Unobserve the element after the first intersection event | `boolean` | `false` |
135135
| intersecting | `true` if the observed element is intersecting the viewport | `boolean` | `false` |
136-
| root | Containing element | `null` or `HTMLElement` | `null` |
136+
| root | Containing element | `Element` \| `Document` \| `null` | `null` |
137137
| rootMargin | Margin offset of the containing element | `string` | `"0px"` |
138138
| threshold | Percentage of element visibility to trigger an event | `number` between 0 and 1, or an array of `number`s between 0 and 1 | `0` |
139139
| entry | Observed element metadata | `null` or [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) | `null` |
@@ -237,9 +237,9 @@ As with the scroll-to-end example, `root` must be an element that scrolls on its
237237

238238
| Name | Description | Type | Default value |
239239
| :------------------- | :---------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
240-
| elements | Array of HTML elements to observe | `Array<HTMLElement \| null>` | `[]` |
240+
| elements | Array of elements to observe | `ReadonlyArray<Element \| null \| undefined>` | `[]` |
241241
| once | Unobserve elements after the first intersection event | `boolean` | `false` |
242-
| root | Containing element | `null` or `HTMLElement` | `null` |
242+
| root | Containing element | `Element` \| `Document` \| `null` | `null` |
243243
| rootMargin | Margin offset of the containing element | `string` | `"0px"` |
244244
| threshold | Percentage of element visibility to trigger an event | `number` between 0 and 1, or an array of `number`s between 0 and 1 | `0` |
245245
| elementIntersections | Map of each element to its intersection state | `Map<HTMLElement \| null, boolean>` | `new Map()` |

src/IntersectionObserver.svelte

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
44
/**
55
* @typedef {Object} Props
6-
* @property {null | HTMLElement} [element] The HTML Element to observe.
6+
* @property {Element | null | undefined} [element] The Element to observe.
77
* @property {boolean} [once] Set to `true` to unobserve the element after it intersects the viewport.
88
* @property {boolean} [intersecting] `true` if the observed element is intersecting the viewport.
9-
* @property {null | HTMLElement} [root] Specify the containing element. Defaults to the browser viewport.
9+
* @property {Element | Document | null | undefined} [root] Specify the containing element. Defaults to the browser viewport.
1010
* @property {string} [rootMargin] Margin offset of the containing element.
1111
* @property {number | number[]} [threshold] Percentage of element visibility to trigger an event. Value must be between 0 and 1.
1212
* @property {null | IntersectionObserverEntry} [entry] Observed element metadata.
@@ -33,7 +33,7 @@
3333
children,
3434
} = $props();
3535
36-
/** @type {null | HTMLElement} */
36+
/** @type {null | Element} */
3737
let prevElement = null;
3838
3939
let prevSkip = untrack(() => skip);
@@ -85,7 +85,7 @@
8585
});
8686
8787
$effect(() => {
88-
const target = element;
88+
const target = element ?? null;
8989
const isSkipped = skip;
9090
const activeObserver = observer;
9191

src/IntersectionObserver.svelte.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import type { Component, Snippet } from "svelte";
22

33
export interface IntersectionObserverProps {
44
/**
5-
* The HTML Element to observe.
5+
* The Element to observe.
66
* @default null
77
*/
8-
element?: null | HTMLElement;
8+
element?: Element | null | undefined;
99

1010
/**
1111
* Set to `true` to unobserve the element
@@ -26,7 +26,7 @@ export interface IntersectionObserverProps {
2626
* Defaults to the browser viewport.
2727
* @default null
2828
*/
29-
root?: null | HTMLElement;
29+
root?: Element | Document | null | undefined;
3030

3131
/**
3232
* Margin offset of the containing element.

src/MultipleIntersectionObserver.svelte

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@
33
44
/**
55
* @typedef {Object} Props
6-
* @property {(HTMLElement | null)[]} [elements] Array of HTML Elements to observe. Use this for better performance when observing multiple elements.
6+
* @property {ReadonlyArray<Element | null | undefined>} [elements] Array of Elements to observe. Use this for better performance when observing multiple elements.
77
* @property {boolean} [once] Set to `true` to unobserve the element after it intersects the viewport.
8-
* @property {null | HTMLElement} [root] Specify the containing element. Defaults to the browser viewport.
8+
* @property {Element | Document | null | undefined} [root] Specify the containing element. Defaults to the browser viewport.
99
* @property {string} [rootMargin] Margin offset of the containing element.
1010
* @property {number | number[]} [threshold] Percentage of element visibility to trigger an event. Value must be between 0 and 1.
11-
* @property {Map<HTMLElement | null, boolean>} [elementIntersections] Map of element to its intersection state.
12-
* @property {Map<HTMLElement | null, IntersectionObserverEntry>} [elementEntries] Map of element to its latest entry.
11+
* @property {Map<Element | null | undefined, boolean>} [elementIntersections] Map of element to its intersection state.
12+
* @property {Map<Element | null | undefined, IntersectionObserverEntry>} [elementEntries] Map of element to its latest entry.
1313
* @property {null | IntersectionObserver} [observer] `IntersectionObserver` instance.
1414
* @property {boolean} [skip] Set to `true` to pause observing all elements without disconnecting the observer or losing `elementIntersections`/`elementEntries` state. Set back to `false` to resume.
15-
* @property {(detail: { entry: IntersectionObserverEntry, target: HTMLElement }) => void} [onobserve] Called when an element is first observed and also whenever an intersection event occurs.
16-
* @property {(detail: { entry: IntersectionObserverEntry & { isIntersecting: true }, target: HTMLElement }) => void} [onintersect] Called only when an element is intersecting the viewport.
17-
* @property {import("svelte").Snippet<[{ observer: null | IntersectionObserver, elementIntersections: Map<HTMLElement | null, boolean>, elementEntries: Map<HTMLElement | null, IntersectionObserverEntry> }]>} [children]
15+
* @property {(detail: { entry: IntersectionObserverEntry, target: Element }) => void} [onobserve] Called when an element is first observed and also whenever an intersection event occurs.
16+
* @property {(detail: { entry: IntersectionObserverEntry & { isIntersecting: true }, target: Element }) => void} [onintersect] Called only when an element is intersecting the viewport.
17+
* @property {import("svelte").Snippet<[{ observer: null | IntersectionObserver, elementIntersections: Map<Element | null | undefined, boolean>, elementEntries: Map<Element | null | undefined, IntersectionObserverEntry> }]>} [children]
1818
*/
1919
2020
/** @type {Props} */
@@ -33,7 +33,7 @@
3333
children,
3434
} = $props();
3535
36-
/** @type {Set<HTMLElement | null>} */
36+
/** @type {Set<Element | null | undefined>} */
3737
let prevElements = new Set();
3838
3939
let prevSkip = untrack(() => skip);
@@ -49,7 +49,7 @@
4949
observer = new IntersectionObserver(
5050
(entries) => {
5151
for (const _entry of entries) {
52-
const target = /** @type {HTMLElement} */ (_entry.target);
52+
const target = /** @type {Element} */ (_entry.target);
5353
5454
elementIntersections.set(target, _entry.isIntersecting);
5555
elementEntries.set(target, _entry);

src/MultipleIntersectionObserver.svelte.d.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import type { Component, Snippet } from "svelte";
22

33
export interface MultipleIntersectionObserverProps {
44
/**
5-
* Array of HTML Elements to observe.
5+
* Array of Elements to observe.
66
* Use this for better performance when observing multiple elements.
77
* @default []
88
*/
9-
elements?: (HTMLElement | null)[];
9+
elements?: ReadonlyArray<Element | null | undefined>;
1010

1111
/**
1212
* Set to `true` to unobserve the element
@@ -20,7 +20,7 @@ export interface MultipleIntersectionObserverProps {
2020
* Defaults to the browser viewport.
2121
* @default null
2222
*/
23-
root?: null | HTMLElement;
23+
root?: Element | Document | null | undefined;
2424

2525
/**
2626
* Margin offset of the containing element.
@@ -39,13 +39,13 @@ export interface MultipleIntersectionObserverProps {
3939
* Map of element to its intersection state.
4040
* @default new Map()
4141
*/
42-
elementIntersections?: Map<HTMLElement | null, boolean>;
42+
elementIntersections?: Map<Element | null | undefined, boolean>;
4343

4444
/**
4545
* Map of element to its latest entry.
4646
* @default new Map()
4747
*/
48-
elementEntries?: Map<HTMLElement | null, IntersectionObserverEntry>;
48+
elementEntries?: Map<Element | null | undefined, IntersectionObserverEntry>;
4949

5050
/**
5151
* `IntersectionObserver` instance.
@@ -67,23 +67,26 @@ export interface MultipleIntersectionObserverProps {
6767
*/
6868
onobserve?: (detail: {
6969
entry: IntersectionObserverEntry;
70-
target: HTMLElement;
70+
target: Element;
7171
}) => void;
7272

7373
/**
7474
* Called only when an element is intersecting the viewport.
7575
*/
7676
onintersect?: (detail: {
7777
entry: IntersectionObserverEntry & { isIntersecting: true };
78-
target: HTMLElement;
78+
target: Element;
7979
}) => void;
8080

8181
children?: Snippet<
8282
[
8383
{
8484
observer: null | IntersectionObserver;
85-
elementIntersections: Map<HTMLElement | null, boolean>;
86-
elementEntries: Map<HTMLElement | null, IntersectionObserverEntry>;
85+
elementIntersections: Map<Element | null | undefined, boolean>;
86+
elementEntries: Map<
87+
Element | null | undefined,
88+
IntersectionObserverEntry
89+
>;
8790
},
8891
]
8992
>;

src/create-intersection-observer.svelte.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export interface IntersectionObserverState {
99
readonly entry: null | IntersectionObserverEntry;
1010

1111
/** Attachment to apply to the observed element via `{@attach}`. */
12-
attach: Attachment<HTMLElement>;
12+
attach: Attachment<Element>;
1313
}
1414

1515
/**

src/create-intersection-observer.svelte.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function createIntersectionObserver(getOptions = () => ({})) {
1414

1515
const attachment = intersectAttachment(getOptions);
1616

17-
/** @param {HTMLElement} node */
17+
/** @param {Element} node */
1818
function attach(node) {
1919
/** @param {Event} event */
2020
const onObserve = (event) => {

src/intersect.svelte.d.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export interface IntersectActionOptions {
77
* Defaults to the browser viewport.
88
* @default null
99
*/
10-
root?: null | HTMLElement;
10+
root?: Element | Document | null;
1111

1212
/**
1313
* Margin offset of the containing element.
@@ -43,7 +43,7 @@ export interface IntersectActionOptions {
4343
* viewport) `CustomEvent`s on the element — listen with `onobserve`/`onintersect`.
4444
*/
4545
export const intersect: Action<
46-
HTMLElement,
46+
Element,
4747
IntersectActionOptions | undefined,
4848
{
4949
onobserve?: (event: CustomEvent<IntersectionObserverEntry>) => void;
@@ -59,7 +59,7 @@ export const intersect: Action<
5959
*/
6060
export function intersectAttachment(
6161
getOptions?: () => IntersectActionOptions,
62-
): Attachment<HTMLElement>;
62+
): Attachment<Element>;
6363

6464
declare module "svelte/elements" {
6565
export interface HTMLAttributes<T> {
@@ -81,7 +81,7 @@ export interface IntersectGroupSharedOptions {
8181
* Defaults to the browser viewport.
8282
* @default null
8383
*/
84-
root?: null | HTMLElement;
84+
root?: Element | Document | null;
8585

8686
/**
8787
* Margin offset of the containing element.
@@ -129,7 +129,7 @@ export interface IntersectionGroup {
129129
* Returns an attachment for a single node in the group. Call once per
130130
* element in a loop, e.g. `{@attach group.attach({ onobserve })}`.
131131
*/
132-
attach(options?: IntersectGroupNodeOptions): Attachment<HTMLElement>;
132+
attach(options?: IntersectGroupNodeOptions): Attachment<Element>;
133133
}
134134

135135
/**

src/intersect.svelte.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { fromAction } from "svelte/attachments";
44
* Svelte action that observes `node` with the Intersection Observer API.
55
* Dispatches `observe` (on every change) and `intersect` (on entering the
66
* viewport) `CustomEvent`s on `node` — listen with `onobserve`/`onintersect`.
7-
* @param {HTMLElement} node
7+
* @param {Element} node
88
* @param {import("./intersect.svelte.d.ts").IntersectActionOptions} [options]
99
*/
1010
export function intersect(node, options = {}) {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<script lang="ts">
2+
import IntersectionObserver from "svelte-intersection-observer";
3+
4+
let ref: undefined | HTMLDivElement;
5+
let includeElement = true;
6+
let intersecting = false;
7+
8+
$: element = includeElement ? ref : undefined;
9+
</script>
10+
11+
<header class:intersecting>
12+
{intersecting ? "Element is in view" : "Element is not in view"}
13+
<button
14+
data-testid="clear"
15+
onclick={() => (includeElement = false)}
16+
>
17+
Clear
18+
</button>
19+
<button
20+
data-testid="restore"
21+
onclick={() => (includeElement = true)}
22+
>
23+
Restore
24+
</button>
25+
</header>
26+
27+
<IntersectionObserver
28+
{element}
29+
bind:intersecting
30+
>
31+
<div bind:this={ref}>Hello world</div>
32+
</IntersectionObserver>
33+
34+
<style>
35+
header {
36+
position: fixed;
37+
top: 0;
38+
left: 0;
39+
width: 100%;
40+
padding: 1rem;
41+
background-color: #e0f7f6;
42+
}
43+
44+
div {
45+
margin-top: calc(100vh + 1px);
46+
height: 38vh;
47+
padding: 1rem;
48+
background-color: #376462;
49+
color: #fff;
50+
}
51+
</style>

0 commit comments

Comments
 (0)