@graphprotocol/gds-react 0.2.0 → 0.2.1
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/dist/GDSContext.d.ts +13 -0
- package/dist/GDSContext.d.ts.map +1 -0
- package/dist/GDSContext.js +4 -0
- package/dist/GDSContext.js.map +1 -0
- package/dist/GDSProvider.d.ts +1 -9
- package/dist/GDSProvider.d.ts.map +1 -1
- package/dist/GDSProvider.js +4 -3
- package/dist/GDSProvider.js.map +1 -1
- package/dist/components/Avatar.d.ts.map +1 -1
- package/dist/components/Avatar.js +2 -2
- package/dist/components/Avatar.js.map +1 -1
- package/dist/components/Breadcrumbs.parts.js +1 -1
- package/dist/components/Breadcrumbs.parts.js.map +1 -1
- package/dist/components/Button.d.ts.map +1 -1
- package/dist/components/Button.js +69 -69
- package/dist/components/Button.js.map +1 -1
- package/dist/components/Card.js +2 -2
- package/dist/components/Card.js.map +1 -1
- package/dist/components/CodeBlock.d.ts +1 -1
- package/dist/components/CodeBlock.parts.d.ts +1 -1
- package/dist/components/CopyButton.d.ts +1 -1
- package/dist/components/CopyButton.d.ts.map +1 -1
- package/dist/components/CopyButton.js +46 -19
- package/dist/components/CopyButton.js.map +1 -1
- package/dist/components/Input.js +2 -2
- package/dist/components/Input.js.map +1 -1
- package/dist/components/Link.js +2 -2
- package/dist/components/Link.js.map +1 -1
- package/dist/components/Menu.parts.d.ts +4 -5
- package/dist/components/Menu.parts.d.ts.map +1 -1
- package/dist/components/Menu.parts.js +49 -44
- package/dist/components/Menu.parts.js.map +1 -1
- package/dist/components/Modal.parts.d.ts.map +1 -1
- package/dist/components/Modal.parts.js +17 -21
- package/dist/components/Modal.parts.js.map +1 -1
- package/dist/components/Pane.d.ts +9 -0
- package/dist/components/Pane.d.ts.map +1 -0
- package/dist/components/Pane.js +8 -0
- package/dist/components/Pane.js.map +1 -0
- package/dist/components/Pane.meta.d.ts +20 -0
- package/dist/components/Pane.meta.d.ts.map +1 -0
- package/dist/components/Pane.meta.js +30 -0
- package/dist/components/Pane.meta.js.map +1 -0
- package/dist/components/Pane.parts.d.ts +77 -0
- package/dist/components/Pane.parts.d.ts.map +1 -0
- package/dist/components/Pane.parts.js +412 -0
- package/dist/components/Pane.parts.js.map +1 -0
- package/dist/components/Search.js +1 -1
- package/dist/components/Tooltip.parts.d.ts +13 -4
- package/dist/components/Tooltip.parts.d.ts.map +1 -1
- package/dist/components/Tooltip.parts.js +51 -63
- package/dist/components/Tooltip.parts.js.map +1 -1
- package/dist/components/base/ButtonOrLink.d.ts +1 -1
- package/dist/components/base/ButtonOrLink.d.ts.map +1 -1
- package/dist/components/base/ButtonOrLink.parts.d.ts +10 -3
- package/dist/components/base/ButtonOrLink.parts.d.ts.map +1 -1
- package/dist/components/base/ButtonOrLink.parts.js +27 -35
- package/dist/components/base/ButtonOrLink.parts.js.map +1 -1
- package/dist/components/base/MaybeButtonOrLink.d.ts +19 -2
- package/dist/components/base/MaybeButtonOrLink.d.ts.map +1 -1
- package/dist/components/base/MaybeButtonOrLink.js +5 -3
- package/dist/components/base/MaybeButtonOrLink.js.map +1 -1
- package/dist/components/base/Presence.d.ts +157 -0
- package/dist/components/base/Presence.d.ts.map +1 -0
- package/dist/components/base/Presence.js +808 -0
- package/dist/components/base/Presence.js.map +1 -0
- package/dist/components/base/index.d.ts +1 -0
- package/dist/components/base/index.d.ts.map +1 -1
- package/dist/components/base/index.js +1 -0
- package/dist/components/base/index.js.map +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -0
- package/dist/components/index.js.map +1 -1
- package/dist/hooks/useCSSProp.js +1 -1
- package/dist/hooks/useCSSProp.js.map +1 -1
- package/dist/hooks/useControlled.d.ts.map +1 -1
- package/dist/hooks/useControlled.js +6 -4
- package/dist/hooks/useControlled.js.map +1 -1
- package/dist/hooks/useGDS.js +1 -1
- package/dist/hooks/useGDS.js.map +1 -1
- package/dist/hooks/useStyleObserver.js +1 -1
- package/dist/hooks/useStyleObserver.js.map +1 -1
- package/dist/tailwind-plugin.d.ts.map +1 -1
- package/dist/tailwind-plugin.js +3 -0
- package/dist/tailwind-plugin.js.map +1 -1
- package/dist/utils/InlineCounter.d.ts +3 -0
- package/dist/utils/InlineCounter.d.ts.map +1 -0
- package/dist/utils/InlineCounter.js +7 -0
- package/dist/utils/InlineCounter.js.map +1 -0
- package/dist/utils/RenderCount.d.ts +3 -0
- package/dist/utils/RenderCount.d.ts.map +1 -0
- package/dist/utils/RenderCount.js +7 -0
- package/dist/utils/RenderCount.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -1
- package/package.json +14 -14
- package/src/GDSContext.ts +16 -0
- package/src/GDSProvider.tsx +20 -31
- package/src/components/Avatar.tsx +3 -2
- package/src/components/Breadcrumbs.parts.tsx +1 -1
- package/src/components/Button.tsx +113 -107
- package/src/components/Card.tsx +2 -2
- package/src/components/CopyButton.tsx +49 -25
- package/src/components/Input.tsx +1 -1
- package/src/components/Link.tsx +2 -2
- package/src/components/Menu.parts.tsx +75 -72
- package/src/components/Modal.parts.tsx +26 -31
- package/src/components/Pane.meta.ts +31 -0
- package/src/components/Pane.parts.tsx +713 -0
- package/src/components/Pane.tsx +17 -0
- package/src/components/Search.tsx +1 -1
- package/src/components/Tooltip.parts.tsx +95 -80
- package/src/components/base/ButtonOrLink.parts.tsx +71 -51
- package/src/components/base/ButtonOrLink.tsx +1 -0
- package/src/components/base/MaybeButtonOrLink.tsx +26 -5
- package/src/components/base/Presence.tsx +1375 -0
- package/src/components/base/index.ts +1 -0
- package/src/components/index.ts +10 -0
- package/src/hooks/useCSSProp.ts +1 -1
- package/src/hooks/useControlled.ts +16 -8
- package/src/hooks/useGDS.ts +1 -1
- package/src/hooks/useStyleObserver.ts +1 -1
- package/src/tailwind-plugin.ts +3 -0
- package/src/utils/InlineCounter.tsx +17 -0
- package/src/utils/RenderCount.tsx +7 -0
- package/src/utils/index.ts +2 -0
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Children, createContext, isValidElement, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
4
|
+
import { flushSync } from 'react-dom';
|
|
5
|
+
import { Render } from "./Render.js";
|
|
6
|
+
const PresenceContext = createContext(null);
|
|
7
|
+
/**
|
|
8
|
+
* Hook to access presence information from the nearest `Presence` item ancestor.
|
|
9
|
+
*
|
|
10
|
+
* @returns An object with `status` and `present`, or `null` if not inside a `Presence` component.
|
|
11
|
+
*/
|
|
12
|
+
export function usePresence() {
|
|
13
|
+
return useContext(PresenceContext);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Presence.
|
|
17
|
+
*
|
|
18
|
+
* - If you pass no `visibleKey` → unmounting mode.
|
|
19
|
+
* - If you pass `visibleKey` → persistent mode.
|
|
20
|
+
*/
|
|
21
|
+
export function Presence({ visibleKey, enterMs = 0, exitMs = 0, initial = false, render, renderChild = _jsx("div", {}), getChildKey, onBeforeTransitionStart, onTransitionStart, onBeforeTransitionEnd, onTransitionEnd, children, }) {
|
|
22
|
+
if (visibleKey !== undefined) {
|
|
23
|
+
return (_jsx(PersistentPresence, { visibleKey: visibleKey, enterMs: enterMs, exitMs: exitMs, initial: initial, render: render, renderChild: renderChild, getChildKey: getChildKey, onBeforeTransitionStart: onBeforeTransitionStart, onTransitionStart: onTransitionStart, onBeforeTransitionEnd: onBeforeTransitionEnd, onTransitionEnd: onTransitionEnd, children: children }));
|
|
24
|
+
}
|
|
25
|
+
return (_jsx(UnmountingPresence, { enterMs: enterMs, exitMs: exitMs, initial: initial, render: render, renderChild: renderChild, getChildKey: getChildKey, onBeforeTransitionStart: onBeforeTransitionStart, onTransitionStart: onTransitionStart, onBeforeTransitionEnd: onBeforeTransitionEnd, onTransitionEnd: onTransitionEnd, children: children }));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* UnmountingPresence.
|
|
29
|
+
*
|
|
30
|
+
* Mental model:
|
|
31
|
+
*
|
|
32
|
+
* - Build a map of all items (previous items + current children).
|
|
33
|
+
* - Update their statuses ("entering" → "visible" when added, "visible" → "exiting" when unmounted,
|
|
34
|
+
* "exiting" → "entering" when re-added).
|
|
35
|
+
* - Sort: streaming merge that walks through previous items, using survivors as anchors. For each
|
|
36
|
+
* survivor, flush all unprocessed current children up to and including it. Exiting items are
|
|
37
|
+
* inserted at their previous position.
|
|
38
|
+
*/
|
|
39
|
+
function UnmountingPresence({ enterMs, exitMs, initial, render, renderChild, getChildKey, onBeforeTransitionStart, onTransitionStart, onBeforeTransitionEnd, onTransitionEnd, children, }) {
|
|
40
|
+
const getChildKeyRef = useRef(getChildKey);
|
|
41
|
+
getChildKeyRef.current = getChildKey;
|
|
42
|
+
const { setElementRef, fireBeforeTransitionEnd, fireCallbacks } = usePresenceCallbacks(onBeforeTransitionStart, onTransitionStart, onBeforeTransitionEnd, onTransitionEnd);
|
|
43
|
+
const [items, setItems] = useState(() => {
|
|
44
|
+
const { array } = buildChildMap(children, getChildKey);
|
|
45
|
+
const isEntering = initial && enterMs > 0;
|
|
46
|
+
return array.map((child, index) => ({
|
|
47
|
+
key: getStableKey(child, index, getChildKey),
|
|
48
|
+
element: child,
|
|
49
|
+
status: isEntering ? 'entering' : 'visible',
|
|
50
|
+
previousStatus: null,
|
|
51
|
+
initial: true,
|
|
52
|
+
}));
|
|
53
|
+
});
|
|
54
|
+
const { timeoutsRef, addEntered, addExited, flushCallbackRef } = useTransitionTimeouts();
|
|
55
|
+
// Memoize to avoid re-running `useLayoutEffect` on every render (all callbacks are stable)
|
|
56
|
+
const callbacks = useMemo(() => ({ setElementRef, fireBeforeTransitionEnd, fireCallbacks }),
|
|
57
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps -- all callbacks are stable (useCallback with [])
|
|
58
|
+
[]);
|
|
59
|
+
// Set up the flush callback to handle batched transition ends
|
|
60
|
+
flushCallbackRef.current = (entered, exited) => {
|
|
61
|
+
callbacks.fireBeforeTransitionEnd(entered, exited);
|
|
62
|
+
setItems((current) => current
|
|
63
|
+
.filter((item) => !exited.has(item.key))
|
|
64
|
+
.map((item) => entered.has(item.key)
|
|
65
|
+
? { ...item, status: 'visible', previousStatus: item.status }
|
|
66
|
+
: item));
|
|
67
|
+
};
|
|
68
|
+
useInitialTransitions(initial, enterMs, () => {
|
|
69
|
+
const keys = new Set();
|
|
70
|
+
items.forEach((item) => {
|
|
71
|
+
if (item.status === 'entering' && !timeoutsRef.current.has(item.key)) {
|
|
72
|
+
keys.add(item.key);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
return keys;
|
|
76
|
+
}, timeoutsRef, callbacks, (key) => setItems((curr) => curr.map((i) => i.key === key ? { ...i, status: 'visible', previousStatus: i.status } : i)));
|
|
77
|
+
useLayoutEffect(() => {
|
|
78
|
+
const { map: presentByKey, keys: childKeys } = buildChildMap(children, getChildKeyRef.current);
|
|
79
|
+
const nextKeySet = new Set(childKeys);
|
|
80
|
+
setItems((prevItems) => {
|
|
81
|
+
const keysToScheduleExitEnd = new Set();
|
|
82
|
+
const keysToScheduleEnterEnd = new Set();
|
|
83
|
+
const keysEntering = new Set();
|
|
84
|
+
const keysExiting = new Set();
|
|
85
|
+
// Build map of items that are currently transitioning (before this update)
|
|
86
|
+
const previouslyTransitioning = new Map();
|
|
87
|
+
const prevByKey = new Map();
|
|
88
|
+
prevItems.forEach((item) => {
|
|
89
|
+
prevByKey.set(item.key, item);
|
|
90
|
+
if (item.status === 'entering' || item.status === 'exiting') {
|
|
91
|
+
previouslyTransitioning.set(item.key, item.status);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
const nextItems = [];
|
|
95
|
+
// Process current children
|
|
96
|
+
childKeys.forEach((key) => {
|
|
97
|
+
const prev = prevByKey.get(key);
|
|
98
|
+
const present = presentByKey.get(key);
|
|
99
|
+
if (prev?.status === 'exiting') {
|
|
100
|
+
// Item coming back from exiting
|
|
101
|
+
cancelTimeout(key, timeoutsRef);
|
|
102
|
+
const status = enterMs > 0 ? 'entering' : 'visible';
|
|
103
|
+
nextItems.push({
|
|
104
|
+
key,
|
|
105
|
+
element: present.element,
|
|
106
|
+
status,
|
|
107
|
+
previousStatus: prev.status,
|
|
108
|
+
initial: false,
|
|
109
|
+
});
|
|
110
|
+
keysEntering.add(key);
|
|
111
|
+
if (status === 'entering') {
|
|
112
|
+
keysToScheduleEnterEnd.add(key);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else if (prev) {
|
|
116
|
+
// Item already exists, keep its status (but instant-transition "entering" → "visible" if `enterMs` changed to 0)
|
|
117
|
+
const status = prev.status === 'entering' && enterMs <= 0 ? 'visible' : prev.status;
|
|
118
|
+
const statusChanged = status !== prev.status;
|
|
119
|
+
nextItems.push({
|
|
120
|
+
key,
|
|
121
|
+
element: present.element,
|
|
122
|
+
status,
|
|
123
|
+
previousStatus: statusChanged ? prev.status : prev.previousStatus,
|
|
124
|
+
initial: prev.initial,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// New item
|
|
129
|
+
const status = enterMs > 0 ? 'entering' : 'visible';
|
|
130
|
+
nextItems.push({
|
|
131
|
+
key,
|
|
132
|
+
element: present.element,
|
|
133
|
+
status,
|
|
134
|
+
previousStatus: 'hidden',
|
|
135
|
+
initial: false,
|
|
136
|
+
});
|
|
137
|
+
keysEntering.add(key);
|
|
138
|
+
if (status === 'entering') {
|
|
139
|
+
keysToScheduleEnterEnd.add(key);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
// Process items being removed
|
|
144
|
+
prevItems.forEach((item) => {
|
|
145
|
+
if (!nextKeySet.has(item.key)) {
|
|
146
|
+
const wasAlreadyExiting = item.status === 'exiting';
|
|
147
|
+
if (!wasAlreadyExiting) {
|
|
148
|
+
cancelTimeout(item.key, timeoutsRef);
|
|
149
|
+
keysExiting.add(item.key);
|
|
150
|
+
if (exitMs > 0) {
|
|
151
|
+
keysToScheduleExitEnd.add(item.key);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Keep exiting items if:
|
|
155
|
+
// - `exitMs` > 0: allows exit transition
|
|
156
|
+
// - already exiting: let it finish (e.g., children changed while item was mid-exit)
|
|
157
|
+
if (exitMs > 0 || wasAlreadyExiting) {
|
|
158
|
+
nextItems.push({
|
|
159
|
+
...item,
|
|
160
|
+
status: 'exiting',
|
|
161
|
+
previousStatus: wasAlreadyExiting ? item.previousStatus : item.status,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
// Fire callbacks (handles both timed and instant transitions)
|
|
167
|
+
callbacks.fireCallbacks(children, enterMs, exitMs, keysEntering, keysExiting, previouslyTransitioning);
|
|
168
|
+
// Sort: preserve order by merging current children with previous items
|
|
169
|
+
const itemsByKey = new Map(nextItems.map((item) => [item.key, item]));
|
|
170
|
+
const childIndexByKey = new Map(childKeys.map((key, index) => [key, index]));
|
|
171
|
+
const result = [];
|
|
172
|
+
const added = new Set();
|
|
173
|
+
let childIndex = 0;
|
|
174
|
+
const addChild = (key) => {
|
|
175
|
+
if (!added.has(key)) {
|
|
176
|
+
const item = itemsByKey.get(key);
|
|
177
|
+
if (item) {
|
|
178
|
+
result.push(item);
|
|
179
|
+
added.add(key);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
// Walk through previous items, inserting current children at their positions
|
|
184
|
+
prevItems.forEach((prevItem) => {
|
|
185
|
+
const item = itemsByKey.get(prevItem.key);
|
|
186
|
+
if (!item)
|
|
187
|
+
return;
|
|
188
|
+
if (item.status === 'exiting') {
|
|
189
|
+
// Exiting items stay at their previous position
|
|
190
|
+
result.push(item);
|
|
191
|
+
added.add(item.key);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
// Current child - add all unprocessed children up to and including it
|
|
195
|
+
const indexInChildren = childIndexByKey.get(prevItem.key);
|
|
196
|
+
if (indexInChildren !== undefined) {
|
|
197
|
+
while (childIndex <= indexInChildren) {
|
|
198
|
+
addChild(childKeys[childIndex]);
|
|
199
|
+
childIndex++;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
// Add any remaining new children
|
|
205
|
+
while (childIndex < childKeys.length) {
|
|
206
|
+
addChild(childKeys[childIndex]);
|
|
207
|
+
childIndex++;
|
|
208
|
+
}
|
|
209
|
+
// Schedule async transitions (after state update completes)
|
|
210
|
+
scheduleTransitionTimeouts(enterMs, exitMs, keysToScheduleEnterEnd, keysToScheduleExitEnd, timeoutsRef, addEntered, addExited);
|
|
211
|
+
return result;
|
|
212
|
+
});
|
|
213
|
+
}, [children, enterMs, exitMs, timeoutsRef, addEntered, addExited, callbacks]);
|
|
214
|
+
// Clear `previousStatus` after the browser has painted the new styles
|
|
215
|
+
useAfterPaint(items.some((item) => item.previousStatus !== null), () => {
|
|
216
|
+
flushSync(() => {
|
|
217
|
+
setItems((current) => current.map((item) => item.previousStatus !== null ? { ...item, previousStatus: null } : item));
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
return (_jsx(PresenceWrapper, { render: render, items: items, children: items.map((item) => (_jsx(PresenceItemProvider, { status: item.status, children: _jsx(Render, { ref: (el) => callbacks.setElementRef(item.key, el), render: renderChild, inert: item.status === 'exiting', "data-key": item.key, "data-status": item.status, "data-starting-style": item.previousStatus !== null || undefined, "data-initial": item.initial || undefined, state: {
|
|
221
|
+
key: item.key,
|
|
222
|
+
status: item.status,
|
|
223
|
+
previousStatus: item.previousStatus,
|
|
224
|
+
initial: item.initial,
|
|
225
|
+
}, children: item.element }) }, item.key))) }));
|
|
226
|
+
}
|
|
227
|
+
function normalizeVisibleKey(input) {
|
|
228
|
+
if (input === null) {
|
|
229
|
+
return new Set();
|
|
230
|
+
}
|
|
231
|
+
if (Array.isArray(input)) {
|
|
232
|
+
return new Set(input);
|
|
233
|
+
}
|
|
234
|
+
// Single key - TypeScript can't narrow the union type here
|
|
235
|
+
return new Set([input]);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* PersistentPresence.
|
|
239
|
+
*
|
|
240
|
+
* - Tracks children by key in a `Map`.
|
|
241
|
+
* - Items are never unmounted _unless_ you stop rendering them at all.
|
|
242
|
+
* - When a key enters `visibleKey`:
|
|
243
|
+
*
|
|
244
|
+
* - status goes "hidden" → "entering" for `enterMs`
|
|
245
|
+
* - then "visible"
|
|
246
|
+
* - When a key leaves `visibleKey`:
|
|
247
|
+
*
|
|
248
|
+
* - status goes "visible" → "exiting" for `exitMs`
|
|
249
|
+
* - then "hidden" (still mounted, but `hidden` attribute on wrapper)
|
|
250
|
+
*/
|
|
251
|
+
function PersistentPresence({ visibleKey, enterMs, exitMs, initial, render, renderChild, getChildKey, onBeforeTransitionStart, onTransitionStart, onBeforeTransitionEnd, onTransitionEnd, children, }) {
|
|
252
|
+
const getChildKeyRef = useRef(getChildKey);
|
|
253
|
+
getChildKeyRef.current = getChildKey;
|
|
254
|
+
const { setElementRef, fireBeforeTransitionEnd, fireCallbacks } = usePresenceCallbacks(onBeforeTransitionStart, onTransitionStart, onBeforeTransitionEnd, onTransitionEnd);
|
|
255
|
+
const [itemsMap, setItemsMap] = useState(() => {
|
|
256
|
+
const { array } = buildChildMap(children, getChildKey);
|
|
257
|
+
const visibleSet = normalizeVisibleKey(visibleKey);
|
|
258
|
+
const map = new Map();
|
|
259
|
+
array.forEach((child, index) => {
|
|
260
|
+
const key = getStableKey(child, index, getChildKey);
|
|
261
|
+
const status = getInitialStatus(visibleSet.has(key), initial, enterMs);
|
|
262
|
+
map.set(key, {
|
|
263
|
+
key,
|
|
264
|
+
element: child,
|
|
265
|
+
status,
|
|
266
|
+
previousStatus: null,
|
|
267
|
+
initial: true,
|
|
268
|
+
order: index,
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
return map;
|
|
272
|
+
});
|
|
273
|
+
const { timeoutsRef, addEntered, addExited, flushCallbackRef } = useTransitionTimeouts();
|
|
274
|
+
// Memoize to avoid re-running `useLayoutEffect` on every render (all callbacks are stable)
|
|
275
|
+
const callbacks = useMemo(() => ({ setElementRef, fireBeforeTransitionEnd, fireCallbacks }),
|
|
276
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps -- all callbacks are stable (useCallback with [])
|
|
277
|
+
[]);
|
|
278
|
+
// Set up the flush callback to handle batched transition ends
|
|
279
|
+
flushCallbackRef.current = (entered, exited) => {
|
|
280
|
+
callbacks.fireBeforeTransitionEnd(entered, exited);
|
|
281
|
+
setItemsMap((current) => {
|
|
282
|
+
const copy = new Map(current);
|
|
283
|
+
exited.forEach((key) => {
|
|
284
|
+
const item = copy.get(key);
|
|
285
|
+
if (item)
|
|
286
|
+
copy.set(key, { ...item, status: 'hidden', previousStatus: item.status });
|
|
287
|
+
});
|
|
288
|
+
entered.forEach((key) => {
|
|
289
|
+
const item = copy.get(key);
|
|
290
|
+
if (item)
|
|
291
|
+
copy.set(key, { ...item, status: 'visible', previousStatus: item.status });
|
|
292
|
+
});
|
|
293
|
+
return copy;
|
|
294
|
+
});
|
|
295
|
+
};
|
|
296
|
+
useInitialTransitions(initial, enterMs, () => {
|
|
297
|
+
const keys = new Set();
|
|
298
|
+
itemsMap.forEach((item) => {
|
|
299
|
+
if (item.status === 'entering' && !timeoutsRef.current.has(item.key)) {
|
|
300
|
+
keys.add(item.key);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
return keys;
|
|
304
|
+
}, timeoutsRef, callbacks, (key) => setItemsMap((curr) => {
|
|
305
|
+
const copy = new Map(curr);
|
|
306
|
+
const item = copy.get(key);
|
|
307
|
+
if (item?.status === 'entering') {
|
|
308
|
+
copy.set(key, { ...item, status: 'visible', previousStatus: item.status });
|
|
309
|
+
}
|
|
310
|
+
return copy;
|
|
311
|
+
}));
|
|
312
|
+
// Sync items with children and handle visibility transitions
|
|
313
|
+
useLayoutEffect(() => {
|
|
314
|
+
const { map: presentByKey } = buildChildMap(children, getChildKeyRef.current);
|
|
315
|
+
const visibleSet = normalizeVisibleKey(visibleKey);
|
|
316
|
+
setItemsMap((prevMap) => {
|
|
317
|
+
const nextMap = new Map(prevMap);
|
|
318
|
+
const keysToScheduleExitEnd = new Set();
|
|
319
|
+
const keysToScheduleEnterEnd = new Set();
|
|
320
|
+
const keysEntering = new Set();
|
|
321
|
+
const keysExiting = new Set();
|
|
322
|
+
// Build map of items that are currently transitioning (before this update)
|
|
323
|
+
const previouslyTransitioning = new Map();
|
|
324
|
+
prevMap.forEach((item) => {
|
|
325
|
+
if (item.status === 'entering' || item.status === 'exiting') {
|
|
326
|
+
previouslyTransitioning.set(item.key, item.status);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
// 1) Update / add all present keys
|
|
330
|
+
presentByKey.forEach(({ element, index }, key) => {
|
|
331
|
+
const existing = nextMap.get(key);
|
|
332
|
+
if (existing) {
|
|
333
|
+
nextMap.set(key, { ...existing, element, order: index });
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
const isVisible = visibleSet.has(key);
|
|
337
|
+
const status = getInitialStatus(isVisible, false, enterMs);
|
|
338
|
+
nextMap.set(key, {
|
|
339
|
+
key,
|
|
340
|
+
element,
|
|
341
|
+
status,
|
|
342
|
+
previousStatus: null,
|
|
343
|
+
initial: false,
|
|
344
|
+
order: index,
|
|
345
|
+
});
|
|
346
|
+
// Track new entering items (for callbacks, even if instant)
|
|
347
|
+
if (isVisible) {
|
|
348
|
+
keysEntering.add(key);
|
|
349
|
+
if (status === 'entering') {
|
|
350
|
+
keysToScheduleEnterEnd.add(key);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
// 2) Remove keys that no longer appear in children
|
|
356
|
+
nextMap.forEach((_item, key) => {
|
|
357
|
+
if (!presentByKey.has(key))
|
|
358
|
+
nextMap.delete(key);
|
|
359
|
+
});
|
|
360
|
+
// 3) Handle visibility transitions for existing items
|
|
361
|
+
nextMap.forEach((item, key) => {
|
|
362
|
+
// Skip newly added items (already handled above)
|
|
363
|
+
if (!prevMap.has(key))
|
|
364
|
+
return;
|
|
365
|
+
const shouldBeVisible = visibleSet.has(key);
|
|
366
|
+
if (shouldBeVisible) {
|
|
367
|
+
if (item.status === 'exiting' || item.status === 'hidden') {
|
|
368
|
+
cancelTimeout(key, timeoutsRef);
|
|
369
|
+
const status = enterMs > 0 ? 'entering' : 'visible';
|
|
370
|
+
nextMap.set(key, { ...item, status, previousStatus: item.status, initial: false });
|
|
371
|
+
keysEntering.add(key);
|
|
372
|
+
if (status === 'entering') {
|
|
373
|
+
keysToScheduleEnterEnd.add(key);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
else if (item.status === 'entering' && enterMs <= 0) {
|
|
377
|
+
nextMap.set(key, { ...item, status: 'visible', previousStatus: item.status });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
else if (item.status === 'visible' || item.status === 'entering') {
|
|
381
|
+
cancelTimeout(key, timeoutsRef);
|
|
382
|
+
const status = exitMs > 0 ? 'exiting' : 'hidden';
|
|
383
|
+
nextMap.set(key, { ...item, status, previousStatus: item.status });
|
|
384
|
+
keysExiting.add(key);
|
|
385
|
+
if (status === 'exiting') {
|
|
386
|
+
keysToScheduleExitEnd.add(key);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
else if (item.status === 'exiting' && exitMs <= 0) {
|
|
390
|
+
nextMap.set(key, { ...item, status: 'hidden', previousStatus: item.status });
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
// Fire callbacks (handles both timed and instant transitions)
|
|
394
|
+
callbacks.fireCallbacks(visibleKey, enterMs, exitMs, keysEntering, keysExiting, previouslyTransitioning);
|
|
395
|
+
// Schedule async transitions (after state update completes)
|
|
396
|
+
scheduleTransitionTimeouts(enterMs, exitMs, keysToScheduleEnterEnd, keysToScheduleExitEnd, timeoutsRef, addEntered, addExited);
|
|
397
|
+
return nextMap;
|
|
398
|
+
});
|
|
399
|
+
}, [children, visibleKey, enterMs, exitMs, timeoutsRef, addEntered, addExited, callbacks]);
|
|
400
|
+
const items = Array.from(itemsMap.values()).sort((a, b) => a.order - b.order);
|
|
401
|
+
// Clear `previousStatus` after the browser has painted the new styles
|
|
402
|
+
useAfterPaint(items.some((item) => item.previousStatus !== null), () => {
|
|
403
|
+
flushSync(() => {
|
|
404
|
+
setItemsMap((current) => {
|
|
405
|
+
const copy = new Map(current);
|
|
406
|
+
copy.forEach((item, key) => {
|
|
407
|
+
if (item.previousStatus !== null) {
|
|
408
|
+
copy.set(key, { ...item, previousStatus: null });
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
return copy;
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
return (_jsx(PresenceWrapper, { render: render, items: items, children: items.map((item) => (_jsx(PresenceItemProvider, { status: item.status, children: _jsx(Render, { ref: (el) => callbacks.setElementRef(item.key, el), render: renderChild, hidden: item.status === 'hidden', inert: item.status === 'exiting', "data-key": item.key, "data-status": item.status, "data-starting-style": item.previousStatus !== null || undefined, "data-initial": item.initial || undefined, state: {
|
|
416
|
+
key: item.key,
|
|
417
|
+
status: item.status,
|
|
418
|
+
previousStatus: item.previousStatus,
|
|
419
|
+
initial: item.initial,
|
|
420
|
+
}, children: item.element }) }, item.key))) }));
|
|
421
|
+
}
|
|
422
|
+
/* -------------------------------------------------------------------------- */
|
|
423
|
+
/* Helper components and utilities */
|
|
424
|
+
/* -------------------------------------------------------------------------- */
|
|
425
|
+
/** Renders a wrapper with computed state if the `render` prop is passed. */
|
|
426
|
+
function PresenceWrapper({ render, items, children, }) {
|
|
427
|
+
if (!render) {
|
|
428
|
+
return children;
|
|
429
|
+
}
|
|
430
|
+
return (_jsx(Render, { render: render, state: {
|
|
431
|
+
transitioning: items.some((item) => item.status === 'entering' || item.status === 'exiting'),
|
|
432
|
+
hasPresent: items.some((item) => item.status === 'entering' || item.status === 'visible'),
|
|
433
|
+
items,
|
|
434
|
+
}, children: children }));
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Provider component that wraps each `Presence` item to provide presence context to descendants.
|
|
438
|
+
* Computes `present` by checking if this item and all ancestor items are present.
|
|
439
|
+
*/
|
|
440
|
+
function PresenceItemProvider({ status, children }) {
|
|
441
|
+
const parentContext = useContext(PresenceContext);
|
|
442
|
+
// Current item is present if it's entering or visible
|
|
443
|
+
const isPresent = status === 'entering' || status === 'visible';
|
|
444
|
+
// Overall present state is true only if this item AND all ancestors are present
|
|
445
|
+
const present = isPresent && (parentContext?.present ?? true);
|
|
446
|
+
return _jsx(PresenceContext, { value: { status, present }, children: children });
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Get a stable key for our internal arrays.
|
|
450
|
+
*
|
|
451
|
+
* - If a custom `getChildKey` function is provided, use it.
|
|
452
|
+
* - Otherwise, use the element's `key` prop (with React's internal prefixes stripped).
|
|
453
|
+
* - Fall back to the index if no key is available.
|
|
454
|
+
*/
|
|
455
|
+
function getStableKey(child, index, getChildKey) {
|
|
456
|
+
// Use custom key function if provided and if it returns a value for this child
|
|
457
|
+
if (getChildKey) {
|
|
458
|
+
const customKey = getChildKey(child, index);
|
|
459
|
+
if (customKey !== undefined) {
|
|
460
|
+
return customKey;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// For valid React elements, use their `key` prop
|
|
464
|
+
if (isValidElement(child)) {
|
|
465
|
+
if (child.key !== null) {
|
|
466
|
+
// Undo prefix added by `Children.toArray` for explicit keys: ".$foo" → "foo"
|
|
467
|
+
if (typeof child.key === 'string' && child.key.startsWith('.$')) {
|
|
468
|
+
const slicedKey = child.key.slice(2);
|
|
469
|
+
if (/^-?\d+(\.\d+)?$/.test(slicedKey)) {
|
|
470
|
+
return Number(slicedKey);
|
|
471
|
+
}
|
|
472
|
+
return slicedKey;
|
|
473
|
+
}
|
|
474
|
+
return child.key;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// Fallback to index for anything else
|
|
478
|
+
return index;
|
|
479
|
+
}
|
|
480
|
+
/** Build a map of child keys to their elements and indices. */
|
|
481
|
+
function buildChildMap(children, getChildKey) {
|
|
482
|
+
const array = Children.toArray(children);
|
|
483
|
+
const map = new Map();
|
|
484
|
+
const keys = [];
|
|
485
|
+
array.forEach((child, index) => {
|
|
486
|
+
const key = getStableKey(child, index, getChildKey);
|
|
487
|
+
keys.push(key);
|
|
488
|
+
map.set(key, { element: child, index });
|
|
489
|
+
});
|
|
490
|
+
return { array, map, keys };
|
|
491
|
+
}
|
|
492
|
+
/** Cancel a pending timeout and remove it from the timeouts map. */
|
|
493
|
+
function cancelTimeout(key, timeoutsRef) {
|
|
494
|
+
const timeoutId = timeoutsRef.current.get(key);
|
|
495
|
+
if (timeoutId !== undefined) {
|
|
496
|
+
window.clearTimeout(timeoutId);
|
|
497
|
+
timeoutsRef.current.delete(key);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Hook that runs a callback after the browser has painted. Uses double `requestAnimationFrame`: the
|
|
502
|
+
* first rAF schedules before the next paint, the second runs after that paint completes.
|
|
503
|
+
*/
|
|
504
|
+
function useAfterPaint(shouldRun, callback) {
|
|
505
|
+
useLayoutEffect(() => {
|
|
506
|
+
if (!shouldRun)
|
|
507
|
+
return;
|
|
508
|
+
let outerFrameId;
|
|
509
|
+
let innerFrameId;
|
|
510
|
+
outerFrameId = requestAnimationFrame(() => {
|
|
511
|
+
innerFrameId = requestAnimationFrame(() => {
|
|
512
|
+
callback();
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
return () => {
|
|
516
|
+
cancelAnimationFrame(outerFrameId);
|
|
517
|
+
cancelAnimationFrame(innerFrameId);
|
|
518
|
+
};
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
/** Determine initial status for an item based on visibility and transition settings. */
|
|
522
|
+
function getInitialStatus(isVisible, initial, enterMs) {
|
|
523
|
+
if (!isVisible)
|
|
524
|
+
return 'hidden';
|
|
525
|
+
return initial && enterMs > 0 ? 'entering' : 'visible';
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Hook to manage transition timeouts with batching. Each key gets its own timeout (so cancelling
|
|
529
|
+
* one doesn't affect others), but callbacks are batched together when timeouts fire in the same
|
|
530
|
+
* event loop tick.
|
|
531
|
+
*/
|
|
532
|
+
function useTransitionTimeouts() {
|
|
533
|
+
const timeoutsRef = useRef(new Map());
|
|
534
|
+
const pendingEnteredRef = useRef(new Set());
|
|
535
|
+
const pendingExitedRef = useRef(new Set());
|
|
536
|
+
const flushScheduledRef = useRef(false);
|
|
537
|
+
const flushCallbackRef = useRef(null);
|
|
538
|
+
useEffect(() => {
|
|
539
|
+
const timeouts = timeoutsRef.current;
|
|
540
|
+
return () => {
|
|
541
|
+
for (const id of timeouts.values()) {
|
|
542
|
+
window.clearTimeout(id);
|
|
543
|
+
}
|
|
544
|
+
timeouts.clear();
|
|
545
|
+
};
|
|
546
|
+
}, []);
|
|
547
|
+
const flush = useCallback(() => {
|
|
548
|
+
flushScheduledRef.current = false;
|
|
549
|
+
const pendingEntered = pendingEnteredRef.current;
|
|
550
|
+
const pendingExited = pendingExitedRef.current;
|
|
551
|
+
if (pendingEntered.size === 0 && pendingExited.size === 0)
|
|
552
|
+
return;
|
|
553
|
+
const entered = new Set(pendingEntered);
|
|
554
|
+
const exited = new Set(pendingExited);
|
|
555
|
+
pendingEntered.clear();
|
|
556
|
+
pendingExited.clear();
|
|
557
|
+
flushCallbackRef.current?.(entered, exited);
|
|
558
|
+
}, []);
|
|
559
|
+
const scheduleFlush = useCallback(() => {
|
|
560
|
+
if (flushScheduledRef.current)
|
|
561
|
+
return;
|
|
562
|
+
flushScheduledRef.current = true;
|
|
563
|
+
// Use setTimeout(0) instead of queueMicrotask because each setTimeout callback is its own
|
|
564
|
+
// macrotask. Microtasks run at the end of the CURRENT macrotask, so queueMicrotask would flush
|
|
565
|
+
// before other setTimeout callbacks with the same delay have a chance to run.
|
|
566
|
+
window.setTimeout(flush, 0);
|
|
567
|
+
}, [flush]);
|
|
568
|
+
const addEntered = useCallback((key) => {
|
|
569
|
+
timeoutsRef.current.delete(key);
|
|
570
|
+
pendingEnteredRef.current.add(key);
|
|
571
|
+
scheduleFlush();
|
|
572
|
+
}, [scheduleFlush]);
|
|
573
|
+
const addExited = useCallback((key) => {
|
|
574
|
+
timeoutsRef.current.delete(key);
|
|
575
|
+
pendingExitedRef.current.add(key);
|
|
576
|
+
scheduleFlush();
|
|
577
|
+
}, [scheduleFlush]);
|
|
578
|
+
return { timeoutsRef, addEntered, addExited, flushCallbackRef };
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Schedule timeouts for entering/exiting transitions. Each key gets its own timeout to ensure
|
|
582
|
+
* cancelling one key's transition doesn't affect others. Callbacks are batched when timeouts fire
|
|
583
|
+
* in the same event loop tick.
|
|
584
|
+
*/
|
|
585
|
+
function scheduleTransitionTimeouts(enterMs, exitMs, keysToScheduleEnterEnd, keysToScheduleExitEnd, timeoutsRef, addEntered, addExited) {
|
|
586
|
+
if (exitMs > 0) {
|
|
587
|
+
keysToScheduleExitEnd.forEach((key) => {
|
|
588
|
+
if (timeoutsRef.current.has(key))
|
|
589
|
+
return;
|
|
590
|
+
const timeoutId = window.setTimeout(() => addExited(key), exitMs);
|
|
591
|
+
timeoutsRef.current.set(key, timeoutId);
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
if (enterMs > 0) {
|
|
595
|
+
keysToScheduleEnterEnd.forEach((key) => {
|
|
596
|
+
if (timeoutsRef.current.has(key))
|
|
597
|
+
return;
|
|
598
|
+
const timeoutId = window.setTimeout(() => addEntered(key), enterMs);
|
|
599
|
+
timeoutsRef.current.set(key, timeoutId);
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
/** Hook to handle initial "entering" → "visible" transitions on mount (when `initial={true}`). */
|
|
604
|
+
function useInitialTransitions(initial, enterMs, getEnteringKeys, timeoutsRef, callbacks, updateItemStatus) {
|
|
605
|
+
const initialCallbacksFiredRef = useRef(false);
|
|
606
|
+
// Fire `onBeforeTransitionStart` and `onTransitionStart` for initial transitions
|
|
607
|
+
useLayoutEffect(() => {
|
|
608
|
+
if (!initial || enterMs <= 0 || initialCallbacksFiredRef.current)
|
|
609
|
+
return;
|
|
610
|
+
const enteringKeys = getEnteringKeys();
|
|
611
|
+
if (enteringKeys.size === 0)
|
|
612
|
+
return;
|
|
613
|
+
initialCallbacksFiredRef.current = true;
|
|
614
|
+
// For initial transitions, elements already exist, so we can fire callbacks directly
|
|
615
|
+
// Use `enterMs > 0` to ensure `fireCallbacks` treats this as a timed transition
|
|
616
|
+
// No previously transitioning items on initial mount
|
|
617
|
+
callbacks.fireCallbacks({}, enterMs, 0, enteringKeys, new Set(), new Map());
|
|
618
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps -- only run on mount
|
|
619
|
+
}, []);
|
|
620
|
+
// Schedule timeouts for "entering" → "visible" transitions
|
|
621
|
+
// This must be a separate effect without a guard because React Strict Mode clears timeouts
|
|
622
|
+
// on cleanup, and they need to be re-scheduled on the second run.
|
|
623
|
+
useEffect(() => {
|
|
624
|
+
if (!initial || enterMs <= 0)
|
|
625
|
+
return;
|
|
626
|
+
const enteringKeys = getEnteringKeys();
|
|
627
|
+
if (enteringKeys.size === 0)
|
|
628
|
+
return;
|
|
629
|
+
enteringKeys.forEach((key) => {
|
|
630
|
+
const timeoutId = window.setTimeout(() => {
|
|
631
|
+
callbacks.fireBeforeTransitionEnd(new Set([key]), new Set());
|
|
632
|
+
updateItemStatus(key);
|
|
633
|
+
timeoutsRef.current.delete(key);
|
|
634
|
+
}, enterMs);
|
|
635
|
+
timeoutsRef.current.set(key, timeoutId);
|
|
636
|
+
});
|
|
637
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps -- only run on mount
|
|
638
|
+
}, []);
|
|
639
|
+
}
|
|
640
|
+
function createEmptyPending() {
|
|
641
|
+
return {
|
|
642
|
+
transitionStartEntering: new Set(),
|
|
643
|
+
transitionStartExiting: new Set(),
|
|
644
|
+
transitionStartPreviously: new Map(),
|
|
645
|
+
beforeEndEntered: new Set(),
|
|
646
|
+
transitionEndEntered: new Set(),
|
|
647
|
+
transitionEndExited: new Set(),
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
/** Hook to manage callback-related refs and helpers for `Presence` components. */
|
|
651
|
+
function usePresenceCallbacks(onBeforeTransitionStart, onTransitionStart, onBeforeTransitionEnd, onTransitionEnd) {
|
|
652
|
+
// Store callbacks in refs to avoid re-creating `useCallback` functions
|
|
653
|
+
const callbackRefs = useRef({
|
|
654
|
+
onBeforeTransitionStart,
|
|
655
|
+
onTransitionStart,
|
|
656
|
+
onBeforeTransitionEnd,
|
|
657
|
+
onTransitionEnd,
|
|
658
|
+
});
|
|
659
|
+
callbackRefs.current = {
|
|
660
|
+
onBeforeTransitionStart,
|
|
661
|
+
onTransitionStart,
|
|
662
|
+
onBeforeTransitionEnd,
|
|
663
|
+
onTransitionEnd,
|
|
664
|
+
};
|
|
665
|
+
const elementRefsRef = useRef(new Map());
|
|
666
|
+
const pendingRef = useRef(createEmptyPending());
|
|
667
|
+
// Guard to prevent double-firing in React Strict Mode
|
|
668
|
+
// Initialize with a unique symbol to avoid collision with any valid `children` value (including `null`)
|
|
669
|
+
const guardRef = useRef(Symbol('unset'));
|
|
670
|
+
const setElementRef = useCallback((key, element) => {
|
|
671
|
+
if (element) {
|
|
672
|
+
elementRefsRef.current.set(key, element);
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
elementRefsRef.current.delete(key);
|
|
676
|
+
}
|
|
677
|
+
}, []);
|
|
678
|
+
/** Resolves keys to elements from refs. */
|
|
679
|
+
const resolveElements = useCallback((keys) => {
|
|
680
|
+
const map = new Map();
|
|
681
|
+
keys.forEach((key) => {
|
|
682
|
+
const element = elementRefsRef.current.get(key);
|
|
683
|
+
if (element)
|
|
684
|
+
map.set(key, element);
|
|
685
|
+
});
|
|
686
|
+
return map;
|
|
687
|
+
}, []);
|
|
688
|
+
/**
|
|
689
|
+
* Fires `onBeforeTransitionEnd` callback and tracks keys for `onTransitionEnd`. Called from:
|
|
690
|
+
*
|
|
691
|
+
* - Render phase for instant exits (`exitMs <= 0`)
|
|
692
|
+
* - `useLayoutEffect` for instant enters (`enterMs <= 0`)
|
|
693
|
+
* - `flushCallbackRef.current` for timed transitions (via `useTransitionTimeouts` batching)
|
|
694
|
+
*/
|
|
695
|
+
const fireBeforeTransitionEnd = useCallback((entered, exited) => {
|
|
696
|
+
if (entered.size === 0 && exited.size === 0)
|
|
697
|
+
return;
|
|
698
|
+
// Track for the `onTransitionEnd` callback (will fire in `useLayoutEffect`)
|
|
699
|
+
entered.forEach((key) => pendingRef.current.transitionEndEntered.add(key));
|
|
700
|
+
exited.forEach((key) => pendingRef.current.transitionEndExited.add(key));
|
|
701
|
+
// Fire the `onBeforeTransitionEnd` callback synchronously
|
|
702
|
+
callbackRefs.current.onBeforeTransitionEnd?.({
|
|
703
|
+
entered: resolveElements(entered),
|
|
704
|
+
exited: resolveElements(exited),
|
|
705
|
+
});
|
|
706
|
+
}, [resolveElements]);
|
|
707
|
+
/**
|
|
708
|
+
* Main entry point for firing callbacks during state updates. Handles both timed and instant
|
|
709
|
+
* transitions:
|
|
710
|
+
*
|
|
711
|
+
* - `onBeforeTransitionStart`: Always fires synchronously (entering keys + exiting elements)
|
|
712
|
+
* - `onTransitionStart` for timed: Tracks for `useLayoutEffect`.
|
|
713
|
+
* - `onTransitionStart` for instant exits: Fires synchronously (element exists, will be unmounted)
|
|
714
|
+
* - `onTransitionStart` for instant enters: Tracks for `useLayoutEffect` (element doesn't exist
|
|
715
|
+
* yet)
|
|
716
|
+
* - `onBeforeTransitionEnd` for instant exits: Fires synchronously.
|
|
717
|
+
* - `onBeforeTransitionEnd` for instant enters: Tracks for `useLayoutEffect`.
|
|
718
|
+
*/
|
|
719
|
+
const fireCallbacks = useCallback((guardValue, enterMs, exitMs, entering, exiting, previouslyTransitioning) => {
|
|
720
|
+
// Guard against React Strict Mode double-firing
|
|
721
|
+
if (guardRef.current === guardValue)
|
|
722
|
+
return;
|
|
723
|
+
if (entering.size === 0 && exiting.size === 0)
|
|
724
|
+
return;
|
|
725
|
+
guardRef.current = guardValue;
|
|
726
|
+
const pending = pendingRef.current;
|
|
727
|
+
const hasInstantExits = exitMs <= 0 && exiting.size > 0;
|
|
728
|
+
const hasInstantEnters = enterMs <= 0 && entering.size > 0;
|
|
729
|
+
// 1. Fire `onBeforeTransitionStart` synchronously (always)
|
|
730
|
+
callbackRefs.current.onBeforeTransitionStart?.({
|
|
731
|
+
entering,
|
|
732
|
+
exiting: resolveElements(exiting),
|
|
733
|
+
previouslyTransitioning,
|
|
734
|
+
});
|
|
735
|
+
// 2. Handle `onTransitionStart`
|
|
736
|
+
// - All enters go to `useLayoutEffect` (element may not exist yet)
|
|
737
|
+
// - Timed exits go to `useLayoutEffect`
|
|
738
|
+
// - Instant exits fire synchronously (element exists, will be unmounted)
|
|
739
|
+
entering.forEach((key) => pending.transitionStartEntering.add(key));
|
|
740
|
+
if (exitMs > 0) {
|
|
741
|
+
exiting.forEach((key) => pending.transitionStartExiting.add(key));
|
|
742
|
+
}
|
|
743
|
+
previouslyTransitioning.forEach((status, key) => {
|
|
744
|
+
if (!pending.transitionStartPreviously.has(key)) {
|
|
745
|
+
pending.transitionStartPreviously.set(key, status);
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
if (hasInstantExits) {
|
|
749
|
+
// Fire `onTransitionStart` synchronously for instant exits only
|
|
750
|
+
callbackRefs.current.onTransitionStart?.({
|
|
751
|
+
entering: new Map(),
|
|
752
|
+
exiting: resolveElements(exiting),
|
|
753
|
+
previouslyTransitioning,
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
// 3. Handle `onBeforeTransitionEnd`
|
|
757
|
+
// - Instant exits: fire synchronously (element exists)
|
|
758
|
+
// - Instant enters: track for `useLayoutEffect` (element doesn't exist yet)
|
|
759
|
+
if (hasInstantExits) {
|
|
760
|
+
fireBeforeTransitionEnd(new Set(), exiting);
|
|
761
|
+
}
|
|
762
|
+
if (hasInstantEnters) {
|
|
763
|
+
entering.forEach((key) => pending.beforeEndEntered.add(key));
|
|
764
|
+
}
|
|
765
|
+
}, [resolveElements, fireBeforeTransitionEnd]);
|
|
766
|
+
// Fire pending callbacks in `useLayoutEffect` (after DOM commit, before paint)
|
|
767
|
+
useLayoutEffect(() => {
|
|
768
|
+
const pending = pendingRef.current;
|
|
769
|
+
const hasStart = pending.transitionStartEntering.size > 0 || pending.transitionStartExiting.size > 0;
|
|
770
|
+
const hasBeforeEnd = pending.beforeEndEntered.size > 0;
|
|
771
|
+
const hasEnd = pending.transitionEndEntered.size > 0 || pending.transitionEndExited.size > 0;
|
|
772
|
+
if (!hasStart && !hasBeforeEnd && !hasEnd)
|
|
773
|
+
return;
|
|
774
|
+
// Capture pending state before resetting
|
|
775
|
+
const { transitionStartEntering, transitionStartExiting, transitionStartPreviously, beforeEndEntered, transitionEndEntered, transitionEndExited, } = pending;
|
|
776
|
+
pendingRef.current = createEmptyPending();
|
|
777
|
+
// Fire `onTransitionStart`
|
|
778
|
+
if (hasStart) {
|
|
779
|
+
callbackRefs.current.onTransitionStart?.({
|
|
780
|
+
entering: resolveElements(transitionStartEntering),
|
|
781
|
+
exiting: resolveElements(transitionStartExiting),
|
|
782
|
+
previouslyTransitioning: transitionStartPreviously,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
// Fire `onBeforeTransitionEnd` for instant enters (this also tracks for `onTransitionEnd`)
|
|
786
|
+
if (hasBeforeEnd) {
|
|
787
|
+
fireBeforeTransitionEnd(beforeEndEntered, new Set());
|
|
788
|
+
}
|
|
789
|
+
// Merge any new entries added by `fireBeforeTransitionEnd` above
|
|
790
|
+
pendingRef.current.transitionEndEntered.forEach((key) => transitionEndEntered.add(key));
|
|
791
|
+
pendingRef.current.transitionEndExited.forEach((key) => transitionEndExited.add(key));
|
|
792
|
+
pendingRef.current.transitionEndEntered = new Set();
|
|
793
|
+
pendingRef.current.transitionEndExited = new Set();
|
|
794
|
+
// Fire `onTransitionEnd` (includes keys from both synchronous and `useLayoutEffect` paths)
|
|
795
|
+
if (transitionEndEntered.size > 0 || transitionEndExited.size > 0) {
|
|
796
|
+
callbackRefs.current.onTransitionEnd?.({
|
|
797
|
+
entered: resolveElements(transitionEndEntered),
|
|
798
|
+
exited: transitionEndExited,
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
return {
|
|
803
|
+
setElementRef,
|
|
804
|
+
fireBeforeTransitionEnd,
|
|
805
|
+
fireCallbacks,
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
//# sourceMappingURL=Presence.js.map
|