view-anchor 0.2.2 → 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -87
- package/README.zh-CN.md +59 -64
- package/dist/index.d.ts +5 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/protocol-publisher.d.ts +7 -9
- package/dist/protocol-publisher.d.ts.map +1 -1
- package/dist/protocol-publisher.js +2 -8
- package/dist/protocol-types.d.ts +5 -8
- package/dist/protocol-types.d.ts.map +1 -1
- package/dist/protocol-types.js +3 -6
- package/dist/protocol.d.ts +2 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +10 -2
- package/dist/react.d.ts +2 -15
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +43 -38
- package/dist/{size-advertiser.d.ts → size-anchor.d.ts} +6 -5
- package/dist/size-anchor.d.ts.map +1 -0
- package/dist/size-anchor.js +131 -0
- package/dist/types.d.ts +26 -32
- package/dist/types.d.ts.map +1 -1
- package/dist/view-anchor.d.ts +52 -33
- package/dist/view-anchor.d.ts.map +1 -1
- package/dist/view-anchor.js +186 -265
- package/docs/bidirectional-design.md +29 -34
- package/docs/index.html +656 -366
- package/docs/mechanism.md +35 -57
- package/docs/performance-report.md +12 -54
- package/docs/protocol.md +12 -3
- package/package.json +4 -6
- package/src/index.ts +5 -19
- package/src/protocol-publisher.ts +10 -16
- package/src/protocol-types.ts +5 -8
- package/src/protocol.ts +13 -7
- package/src/react.ts +51 -64
- package/src/size-anchor.ts +131 -0
- package/src/types.ts +29 -37
- package/src/view-anchor.ts +221 -287
- package/dist/measure-loop.d.ts +0 -24
- package/dist/measure-loop.d.ts.map +0 -1
- package/dist/measure-loop.js +0 -89
- package/dist/size-advertiser.d.ts.map +0 -1
- package/dist/size-advertiser.js +0 -94
- package/src/measure-loop.ts +0 -101
- package/src/size-advertiser.ts +0 -108
package/src/react.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
createViewAnchor,
|
|
5
|
-
type PlacementAnchorHandle,
|
|
6
|
-
type PlacementAnchorOptions,
|
|
7
|
-
} from './view-anchor.js'
|
|
8
|
-
import type { Bounds, ViewAnchorHandle, ViewAnchorOptions } from './types.js'
|
|
1
|
+
import { useCallback, useEffect, useInsertionEffect, useRef, version } from 'react'
|
|
2
|
+
import { createViewAnchor, type ViewAnchorHandle } from './view-anchor.js'
|
|
3
|
+
import type { ViewAnchorOptions } from './view-anchor.js'
|
|
9
4
|
|
|
10
5
|
export interface UseViewAnchorOptions extends ViewAnchorOptions {
|
|
11
6
|
/**
|
|
@@ -18,24 +13,13 @@ export interface UseViewAnchorOptions extends ViewAnchorOptions {
|
|
|
18
13
|
/** Compatible with React 18's null callback and React 19's ref cleanup. */
|
|
19
14
|
export type ViewAnchorRef = (el: HTMLElement | null) => void | (() => void)
|
|
20
15
|
|
|
21
|
-
export interface UsePlacementAnchorOptions extends PlacementAnchorOptions {
|
|
22
|
-
/**
|
|
23
|
-
* Values that re-apply the anchor when changed. Keep this array's length
|
|
24
|
-
* stable across renders.
|
|
25
|
-
*/
|
|
26
|
-
deps?: ReadonlyArray<unknown>
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Callback ref for the explicit-visibility Placement API. */
|
|
30
|
-
export type PlacementAnchorRef = ViewAnchorRef
|
|
31
|
-
|
|
32
16
|
type AnchorHandle = { dispose(): void }
|
|
33
17
|
|
|
34
18
|
interface LifecycleAdapter<Options, Handle extends AnchorHandle> {
|
|
35
19
|
create(target: HTMLElement, options: Options): Handle
|
|
36
20
|
update(handle: Handle, options: Options): void
|
|
37
21
|
collapse(handle: Handle, options: Options): void
|
|
38
|
-
isCollapsed(options: Options): boolean
|
|
22
|
+
isCollapsed(handle: Handle, options: Options): boolean
|
|
39
23
|
}
|
|
40
24
|
|
|
41
25
|
// Callback refs own the imperative anchor because React invokes them during commit.
|
|
@@ -50,12 +34,16 @@ function useAnchorRef<Options, Handle extends AnchorHandle>(
|
|
|
50
34
|
const handleRef = useRef<Handle | null>(null)
|
|
51
35
|
const elementRef = useRef<HTMLElement | null>(null)
|
|
52
36
|
const optionsRef = useRef(options)
|
|
53
|
-
optionsRef.current = options
|
|
54
37
|
const adapterRef = useRef(adapter)
|
|
55
|
-
adapterRef.current = adapter
|
|
56
38
|
const appliedRef = useRef(applied)
|
|
57
39
|
const currentAppliedRef = useRef(applied)
|
|
58
|
-
|
|
40
|
+
// Renders discarded before commit (e.g. suspended transitions) must not
|
|
41
|
+
// update optionsRef. Insertion effects run before callback refs in the same commit.
|
|
42
|
+
useInsertionEffect(() => {
|
|
43
|
+
optionsRef.current = options
|
|
44
|
+
adapterRef.current = adapter
|
|
45
|
+
currentAppliedRef.current = applied
|
|
46
|
+
})
|
|
59
47
|
// Options handed to the adapter on the last create/update call.
|
|
60
48
|
// Tracks applied state across renders where the deps array reference changes.
|
|
61
49
|
const lastAppliedOptionsRef = useRef(options)
|
|
@@ -69,7 +57,7 @@ function useAnchorRef<Options, Handle extends AnchorHandle>(
|
|
|
69
57
|
const handle = handleRef.current
|
|
70
58
|
if (!handle) return
|
|
71
59
|
const adapter = adapterRef.current
|
|
72
|
-
const alreadyCollapsed = adapter.isCollapsed(lastAppliedOptionsRef.current)
|
|
60
|
+
const alreadyCollapsed = adapter.isCollapsed(handle, lastAppliedOptionsRef.current)
|
|
73
61
|
try {
|
|
74
62
|
if (!alreadyCollapsed) adapter.collapse(handle, optionsRef.current)
|
|
75
63
|
} finally {
|
|
@@ -87,10 +75,15 @@ function useAnchorRef<Options, Handle extends AnchorHandle>(
|
|
|
87
75
|
})
|
|
88
76
|
}
|
|
89
77
|
|
|
78
|
+
// React 19 invokes the cleanup returned from a callback ref instead of calling
|
|
79
|
+
// ref(null). React 18 warns about the returned function and still calls ref(null).
|
|
80
|
+
const detachCleanup = (element: HTMLElement): (() => void) | undefined =>
|
|
81
|
+
Number.parseInt(version, 10) >= 19 ? () => deferDetach(element) : undefined
|
|
82
|
+
|
|
90
83
|
const ref = useCallback<ViewAnchorRef>((element) => {
|
|
91
84
|
if (element === elementRef.current) {
|
|
92
85
|
cancelPendingDetach()
|
|
93
|
-
return element ? (
|
|
86
|
+
return element ? detachCleanup(element) : undefined
|
|
94
87
|
}
|
|
95
88
|
|
|
96
89
|
cancelPendingDetach()
|
|
@@ -110,7 +103,7 @@ function useAnchorRef<Options, Handle extends AnchorHandle>(
|
|
|
110
103
|
handleRef.current = adapterRef.current.create(element, optionsRef.current)
|
|
111
104
|
appliedRef.current = currentAppliedRef.current
|
|
112
105
|
lastAppliedOptionsRef.current = optionsRef.current
|
|
113
|
-
return (
|
|
106
|
+
return detachCleanup(element)
|
|
114
107
|
}
|
|
115
108
|
return undefined
|
|
116
109
|
}, [])
|
|
@@ -141,62 +134,56 @@ function useAnchorRef<Options, Handle extends AnchorHandle>(
|
|
|
141
134
|
return ref
|
|
142
135
|
}
|
|
143
136
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
137
|
+
// Whether each handle's latest { visible: false } was accepted. The core does
|
|
138
|
+
// not retry a rejected hidden placement, so unmount must send it again.
|
|
139
|
+
type HideState = { accepted: boolean }
|
|
140
|
+
const hideStates = new WeakMap<ViewAnchorHandle, HideState>()
|
|
141
|
+
|
|
142
|
+
const trackHide = (options: ViewAnchorOptions, state: HideState): ViewAnchorOptions => ({
|
|
143
|
+
...options,
|
|
144
|
+
publish(placement) {
|
|
145
|
+
if (placement.visible) return options.publish(placement)
|
|
146
|
+
state.accepted = false
|
|
147
|
+
const result = options.publish(placement)
|
|
148
|
+
state.accepted = result !== false
|
|
149
|
+
return result
|
|
154
150
|
},
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** Bind zero-bounds visibility to a DOM element callback ref. */
|
|
158
|
-
export function useViewAnchor(options: UseViewAnchorOptions): ViewAnchorRef {
|
|
159
|
-
return useAnchorRef(
|
|
160
|
-
options,
|
|
161
|
-
[options.present, options.publish, ...(options.deps ?? [])],
|
|
162
|
-
viewAdapter,
|
|
163
|
-
)
|
|
164
|
-
}
|
|
151
|
+
})
|
|
165
152
|
|
|
166
|
-
const
|
|
167
|
-
create
|
|
153
|
+
const viewAdapter: LifecycleAdapter<ViewAnchorOptions, ViewAnchorHandle> = {
|
|
154
|
+
create(target, options) {
|
|
155
|
+
const state: HideState = { accepted: false }
|
|
156
|
+
const handle = createViewAnchor(target, trackHide(options, state))
|
|
157
|
+
hideStates.set(handle, state)
|
|
158
|
+
return handle
|
|
159
|
+
},
|
|
168
160
|
update(handle, options) {
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
handle.update(
|
|
172
|
-
...options,
|
|
173
|
-
guardDisplayNone: options.guardDisplayNone ?? false,
|
|
174
|
-
followScroll: options.followScroll ?? false,
|
|
175
|
-
followGeometry: options.followGeometry ?? false,
|
|
176
|
-
})
|
|
161
|
+
// Options are forwarded as-is; the hook applies the same "omitted resets
|
|
162
|
+
// to default" rule as the core's update().
|
|
163
|
+
handle.update(trackHide(options, hideStates.get(handle)!))
|
|
177
164
|
},
|
|
178
165
|
collapse(handle, options) {
|
|
179
|
-
handle.update({ ...options, visible: false })
|
|
166
|
+
handle.update(trackHide({ ...options, visible: false }, hideStates.get(handle)!))
|
|
180
167
|
},
|
|
181
|
-
isCollapsed(options) {
|
|
182
|
-
return !options.visible
|
|
168
|
+
isCollapsed(handle, options) {
|
|
169
|
+
return !options.visible && hideStates.get(handle)!.accepted
|
|
183
170
|
},
|
|
184
171
|
}
|
|
185
172
|
|
|
186
173
|
/** Bind the explicit Placement API to a DOM element callback ref. */
|
|
187
|
-
export function
|
|
174
|
+
export function useViewAnchor(options: UseViewAnchorOptions): ViewAnchorRef {
|
|
188
175
|
return useAnchorRef(
|
|
189
176
|
options,
|
|
190
177
|
[
|
|
191
178
|
options.visible,
|
|
192
179
|
options.publish,
|
|
193
|
-
options.
|
|
180
|
+
options.treatZeroAreaAsHidden,
|
|
194
181
|
options.followScroll,
|
|
195
182
|
options.followGeometry,
|
|
183
|
+
options.holdSelector,
|
|
184
|
+
options.dedupe,
|
|
196
185
|
...(options.deps ?? []),
|
|
197
186
|
],
|
|
198
|
-
|
|
187
|
+
viewAdapter,
|
|
199
188
|
)
|
|
200
189
|
}
|
|
201
|
-
|
|
202
|
-
export type { Bounds }
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { SizeAnchorOptions, SizeAnchorHandle } from './types.js'
|
|
2
|
+
import { watchAbort } from './abort.js'
|
|
3
|
+
|
|
4
|
+
// Replaces a disposed instance's publish callback so a retained handle does
|
|
5
|
+
// not keep the caller's original callback (and whatever it captured) alive.
|
|
6
|
+
const NOOP_PUBLISH = (): false => false
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Report content size for a single axis back to the host.
|
|
10
|
+
*
|
|
11
|
+
* Runs in a downstream document, reads the content size from
|
|
12
|
+
* `ResizeObserverEntry.borderBoxSize` without triggering reflow, and
|
|
13
|
+
* publishes at most once per animation frame, skipping unchanged extents by
|
|
14
|
+
* default (see the `dedupe` option).
|
|
15
|
+
*
|
|
16
|
+
* Measurements are rounded to integer pixels and clamped to >= 0.
|
|
17
|
+
*
|
|
18
|
+
* `target` must be a shrink-to-fit wrapper on the owned axis.
|
|
19
|
+
* If its size is driven by the host view itself (such as `<body>` or `<html>`),
|
|
20
|
+
* updates will not shrink back to content size.
|
|
21
|
+
*/
|
|
22
|
+
export function createSizeAnchor(target: HTMLElement, opts: SizeAnchorOptions): SizeAnchorHandle {
|
|
23
|
+
const axis = opts.axis
|
|
24
|
+
let dedupe = opts.dedupe ?? true
|
|
25
|
+
let publish = opts.publish
|
|
26
|
+
let observer: ResizeObserver | null = null
|
|
27
|
+
let rafId: number | null = null
|
|
28
|
+
let disposed = false
|
|
29
|
+
// Latest border-box recorded by the ResizeObserver callback.
|
|
30
|
+
let latestBoxSize: ResizeObserverSize | null = null
|
|
31
|
+
// Last extent passed to publish; null until one is accepted.
|
|
32
|
+
let lastPublished: number | null = null
|
|
33
|
+
let publicationRevision = 0
|
|
34
|
+
|
|
35
|
+
const measure = (): number | null => {
|
|
36
|
+
if (!latestBoxSize) return null
|
|
37
|
+
const raw = axis === 'block' ? latestBoxSize.blockSize : latestBoxSize.inlineSize
|
|
38
|
+
if (!Number.isFinite(raw)) return null
|
|
39
|
+
return Math.max(0, Math.round(raw))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const publishExtent = (extent: number): void => {
|
|
43
|
+
const previous = lastPublished
|
|
44
|
+
const attempt = ++publicationRevision
|
|
45
|
+
lastPublished = extent
|
|
46
|
+
try {
|
|
47
|
+
const accepted = publish({ axis, extent }) !== false
|
|
48
|
+
// A reentrant update() or dispose() during publish() already moved the
|
|
49
|
+
// baseline; do not roll back over that newer state.
|
|
50
|
+
if (!accepted && publicationRevision === attempt && !disposed) lastPublished = previous
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (publicationRevision === attempt && !disposed) lastPublished = previous
|
|
53
|
+
throw error
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const publishFrame = (): void => {
|
|
58
|
+
rafId = null
|
|
59
|
+
if (disposed) return
|
|
60
|
+
const extent = measure()
|
|
61
|
+
if (extent === null) return
|
|
62
|
+
if (dedupe && extent === lastPublished) return
|
|
63
|
+
publishExtent(extent)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const onResize: ResizeObserverCallback = (entries) => {
|
|
67
|
+
// A callback queued before disconnect() can still fire once more; do not
|
|
68
|
+
// let it write `latestBoxSize` after dispose() has already cleared it.
|
|
69
|
+
if (disposed) return
|
|
70
|
+
const entry = entries[entries.length - 1]
|
|
71
|
+
if (entry) {
|
|
72
|
+
latestBoxSize = entry.borderBoxSize?.[0] ?? entry.contentBoxSize?.[0] ?? latestBoxSize
|
|
73
|
+
}
|
|
74
|
+
if (rafId === null) rafId = requestAnimationFrame(publishFrame)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Warn if measuring body or documentElement, whose size matches the view.
|
|
78
|
+
const doc = target.ownerDocument
|
|
79
|
+
if (target === doc.body || target === doc.documentElement) {
|
|
80
|
+
console.warn(
|
|
81
|
+
`[view-anchor] size-anchor: <${target === doc.body ? 'body' : 'html'}>'s ` +
|
|
82
|
+
`${axis} size is the view size, not content size. The size anchor will ` +
|
|
83
|
+
`never shrink to content; measure a shrink-to-fit wrapper instead.`,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let removeAbortListener = (): void => {}
|
|
88
|
+
|
|
89
|
+
const dispose = (): void => {
|
|
90
|
+
if (disposed) return
|
|
91
|
+
disposed = true
|
|
92
|
+
removeAbortListener()
|
|
93
|
+
removeAbortListener = (): void => {}
|
|
94
|
+
if (rafId !== null) {
|
|
95
|
+
cancelAnimationFrame(rafId)
|
|
96
|
+
rafId = null
|
|
97
|
+
}
|
|
98
|
+
if (observer) {
|
|
99
|
+
observer.disconnect()
|
|
100
|
+
observer = null
|
|
101
|
+
}
|
|
102
|
+
publish = NOOP_PUBLISH
|
|
103
|
+
latestBoxSize = null
|
|
104
|
+
lastPublished = null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (opts.signal?.aborted) dispose()
|
|
108
|
+
else {
|
|
109
|
+
removeAbortListener = watchAbort(opts.signal, dispose)
|
|
110
|
+
observer = new ResizeObserver(onResize)
|
|
111
|
+
observer.observe(target)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
update(next: Omit<SizeAnchorOptions, 'signal' | 'axis'>): void {
|
|
116
|
+
if (disposed) return
|
|
117
|
+
publish = next.publish
|
|
118
|
+
dedupe = next.dedupe ?? true
|
|
119
|
+
// The new callback has not accepted anything yet, so an extent the old
|
|
120
|
+
// callback rejected (or never saw) must not stay latched as the dedupe
|
|
121
|
+
// baseline — otherwise a later identical tick would be silently
|
|
122
|
+
// deduped against a value this callback was never actually given.
|
|
123
|
+
lastPublished = null
|
|
124
|
+
// Re-publish the current size to the new sink immediately so it is not
|
|
125
|
+
// empty until the next ResizeObserver tick.
|
|
126
|
+
const extent = measure()
|
|
127
|
+
if (extent !== null) publishExtent(extent)
|
|
128
|
+
},
|
|
129
|
+
dispose,
|
|
130
|
+
}
|
|
131
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -27,54 +27,46 @@ export type Publisher<T> = (value: T) => PublishResult
|
|
|
27
27
|
*/
|
|
28
28
|
export type Placement = { visible: true; bounds: Bounds } | { visible: false }
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Whether the native view should be attached. When false, publishes
|
|
33
|
-
* zero bounds ({ x: 0, y: 0, width: 0, height: 0 }) so the host can detach
|
|
34
|
-
* the view while keeping its instance alive.
|
|
35
|
-
*/
|
|
36
|
-
present: boolean
|
|
37
|
-
/** Receives the live rect, or zero bounds when detached. */
|
|
38
|
-
publish: Publisher<Bounds>
|
|
39
|
-
/** Stops this anchor when aborted. An already-aborted signal starts no work. */
|
|
40
|
-
signal?: AbortSignal
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface ViewAnchorHandle {
|
|
44
|
-
/** Apply new options and re-publish immediately. */
|
|
45
|
-
update(opts: ViewAnchorOptions): void
|
|
46
|
-
/** Stop observing and clean up listeners. After disposal no further values are published. */
|
|
47
|
-
dispose(): void
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// --- Reverse direction: size advertiser ---
|
|
30
|
+
// --- Reverse direction: size anchor ---
|
|
51
31
|
//
|
|
52
32
|
// Runs in a downstream document to report content size back to the host,
|
|
53
33
|
// allowing the host's DOM placeholder to match the content.
|
|
54
34
|
|
|
55
|
-
/** Which axis this
|
|
56
|
-
export type
|
|
35
|
+
/** Which axis this size anchor reports: 'block' (height) or 'inline' (width). */
|
|
36
|
+
export type SizeAxis = 'block' | 'inline'
|
|
57
37
|
|
|
58
|
-
/** One
|
|
59
|
-
export interface
|
|
60
|
-
/** The axis this
|
|
61
|
-
readonly axis:
|
|
62
|
-
/**
|
|
38
|
+
/** One measured size on the owned axis. */
|
|
39
|
+
export interface SizeMeasurement {
|
|
40
|
+
/** The axis this size anchor reports ('block' or 'inline'). */
|
|
41
|
+
readonly axis: SizeAxis
|
|
42
|
+
/**
|
|
43
|
+
* The target's border-box size on this axis in CSS pixels, rounded and
|
|
44
|
+
* non-negative. Falls back to the content box where no border box is reported.
|
|
45
|
+
*/
|
|
63
46
|
readonly extent: number
|
|
64
47
|
}
|
|
65
48
|
|
|
66
|
-
export interface
|
|
67
|
-
/** The single axis this
|
|
68
|
-
axis:
|
|
69
|
-
/** Receives each
|
|
70
|
-
publish: Publisher<
|
|
71
|
-
/** Stops this
|
|
49
|
+
export interface SizeAnchorOptions {
|
|
50
|
+
/** The single axis this size anchor owns. Fixed for its lifetime. */
|
|
51
|
+
axis: SizeAxis
|
|
52
|
+
/** Receives each measured size. */
|
|
53
|
+
publish: Publisher<SizeMeasurement>
|
|
54
|
+
/** Stops this size anchor when aborted. An already-aborted signal starts no work. */
|
|
72
55
|
signal?: AbortSignal
|
|
56
|
+
/**
|
|
57
|
+
* When true (the default), a measurement identical to the last published
|
|
58
|
+
* extent is not published again. When false, every measurement publishes,
|
|
59
|
+
* even an unchanged extent. Omitting it in update() resets to true.
|
|
60
|
+
*/
|
|
61
|
+
dedupe?: boolean
|
|
73
62
|
}
|
|
74
63
|
|
|
75
|
-
export interface
|
|
76
|
-
/**
|
|
77
|
-
|
|
64
|
+
export interface SizeAnchorHandle {
|
|
65
|
+
/**
|
|
66
|
+
* Apply a full set of options and re-publish the current size
|
|
67
|
+
* immediately. Like creation, an omitted dedupe resets to true.
|
|
68
|
+
*/
|
|
69
|
+
update(opts: Omit<SizeAnchorOptions, 'signal' | 'axis'>): void
|
|
78
70
|
/** Stop observing and cancel any pending animation frame. */
|
|
79
71
|
dispose(): void
|
|
80
72
|
}
|