@trackunit/react-drawer 2.6.71 → 2.6.73
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/index.cjs.js +114 -3
- package/index.esm.js +115 -4
- package/migrations/entry.js.map +1 -0
- package/package.json +2 -2
- package/src/components/Drawer/Drawer.d.ts +12 -1
- package/src/components/Drawer/useDrawer.d.ts +27 -0
package/index.cjs.js
CHANGED
|
@@ -308,7 +308,7 @@ const PANEL_FOCUSABLE_SELECTOR = [
|
|
|
308
308
|
* ```
|
|
309
309
|
* @param {DrawerProps} props - The props for the Drawer component
|
|
310
310
|
*/
|
|
311
|
-
const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "data-testid": dataTestId, className, renderInPortal = false, containerClassName, ariaLabel, ariaLabelledBy, }) => {
|
|
311
|
+
const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "data-testid": dataTestId, className, renderInPortal = false, containerClassName, ariaLabel, ariaLabelledBy, onExitComplete, }) => {
|
|
312
312
|
const { isSm } = reactComponents.useViewportBreakpoints();
|
|
313
313
|
const shouldUsePortal = !isSm || renderInPortal;
|
|
314
314
|
const panelRef = react.useRef(null);
|
|
@@ -316,6 +316,10 @@ const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "d
|
|
|
316
316
|
// the portal (or first paint) has attached the DOM node — refs alone do not.
|
|
317
317
|
const [panelElement, setPanelElement] = react.useState(null);
|
|
318
318
|
const isOpenRef = react.useRef(isOpen);
|
|
319
|
+
const onExitCompleteRef = react.useRef(onExitComplete);
|
|
320
|
+
// Guards against firing onExitComplete twice for the same close cycle if the real
|
|
321
|
+
// transitionend and the headless RAF fallback both resolve the same exit-complete.
|
|
322
|
+
const hasFiredExitCompleteRef = react.useRef(false);
|
|
319
323
|
const [drawerState, dispatch] = react.useReducer(drawerAnimationReducer, {
|
|
320
324
|
...INITIAL_DRAWER_ANIMATION_STATE,
|
|
321
325
|
// Mount off-screen when initially open so the enter slide still plays.
|
|
@@ -325,7 +329,22 @@ const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "d
|
|
|
325
329
|
react.useEffect(() => {
|
|
326
330
|
isOpenRef.current = isOpen;
|
|
327
331
|
}, [isOpen]);
|
|
332
|
+
react.useEffect(() => {
|
|
333
|
+
onExitCompleteRef.current = onExitComplete;
|
|
334
|
+
}, [onExitComplete]);
|
|
335
|
+
const fireExitComplete = react.useCallback(() => {
|
|
336
|
+
if (hasFiredExitCompleteRef.current) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
hasFiredExitCompleteRef.current = true;
|
|
340
|
+
onExitCompleteRef.current?.();
|
|
341
|
+
}, []);
|
|
328
342
|
react.useLayoutEffect(() => {
|
|
343
|
+
if (!isOpen) {
|
|
344
|
+
// A new close cycle is starting — arm the guard so this cycle's genuine
|
|
345
|
+
// exit-complete (real transitionend or headless RAF fallback) can fire.
|
|
346
|
+
hasFiredExitCompleteRef.current = false;
|
|
347
|
+
}
|
|
329
348
|
dispatch({ type: isOpen ? "open" : "close" });
|
|
330
349
|
}, [isOpen]);
|
|
331
350
|
// Defer the open flip until after paint. A single useLayoutEffect flip commits
|
|
@@ -360,9 +379,23 @@ const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "d
|
|
|
360
379
|
const runningAnimations = typeof panelNode.getAnimations === "function" ? panelNode.getAnimations() : [];
|
|
361
380
|
if (runningAnimations.length === 0) {
|
|
362
381
|
dispatch({ type: "transitionEnd" });
|
|
382
|
+
fireExitComplete();
|
|
363
383
|
}
|
|
364
384
|
});
|
|
365
385
|
return () => cancelAnimationFrame(rafId);
|
|
386
|
+
}, [drawerState.mode, drawerState.shouldRender, fireExitComplete]);
|
|
387
|
+
// If transitionend is suppressed while a CSS animation is reported as running
|
|
388
|
+
// (Playwright/iframe), unmount after the motion duration so close cannot stick.
|
|
389
|
+
react.useEffect(() => {
|
|
390
|
+
if (drawerState.mode !== "closed" || !drawerState.shouldRender) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const timeoutId = window.setTimeout(() => {
|
|
394
|
+
if (!isOpenRef.current) {
|
|
395
|
+
dispatch({ type: "transitionEnd" });
|
|
396
|
+
}
|
|
397
|
+
}, DRAWER_TRANSITION_DURATION_MS + 50);
|
|
398
|
+
return () => window.clearTimeout(timeoutId);
|
|
366
399
|
}, [drawerState.mode, drawerState.shouldRender]);
|
|
367
400
|
// If transitionend is suppressed, settle enter after the motion duration so focus
|
|
368
401
|
// still activates. Do not use a zero-delay rAF here — getAnimations() can be empty
|
|
@@ -405,9 +438,20 @@ const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "d
|
|
|
405
438
|
return;
|
|
406
439
|
}
|
|
407
440
|
if (transitionEvent.propertyName === "transform") {
|
|
441
|
+
// A transform transitionend while mode is still "closed" but isOpen has already
|
|
442
|
+
// flipped back to true is an interrupted exit (the reopen's openVisual is still
|
|
443
|
+
// behind its double-rAF), not an exit-complete. Same guard the headless rAF
|
|
444
|
+
// fallback above already applies.
|
|
445
|
+
if (drawerState.mode === "closed" && isOpenRef.current) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const isGenuineExitComplete = drawerState.mode === "closed";
|
|
408
449
|
dispatch({ type: "transitionEnd" });
|
|
450
|
+
if (isGenuineExitComplete) {
|
|
451
|
+
fireExitComplete();
|
|
452
|
+
}
|
|
409
453
|
}
|
|
410
|
-
}, []);
|
|
454
|
+
}, [drawerState.mode, fireExitComplete]);
|
|
411
455
|
if (!drawerState.shouldRender) {
|
|
412
456
|
return null;
|
|
413
457
|
}
|
|
@@ -477,6 +521,48 @@ const useDrawer = (props) => {
|
|
|
477
521
|
}), [dismissProp]);
|
|
478
522
|
const [internalIsOpen, setIsOpen] = react.useState(defaultOpen ?? false);
|
|
479
523
|
const isOpen = typeof controlledIsOpen === "boolean" ? controlledIsOpen : internalIsOpen;
|
|
524
|
+
// `false` initially / whenever never opened. Flips to `true` the moment the
|
|
525
|
+
// resolved `isOpen` value above transitions `true -> false`, and back to
|
|
526
|
+
// `false` either when it transitions `false -> true` again or once Drawer's
|
|
527
|
+
// `onExitComplete` signals the panel has genuinely finished exiting
|
|
528
|
+
// (`handleExitComplete`). See `UseDrawerReturnValue.isExiting`.
|
|
529
|
+
const [isExiting, setIsExiting] = react.useState(false);
|
|
530
|
+
// Tracks the resolved `isOpen` value as of the last render for which
|
|
531
|
+
// `isExiting` was derived below, so the render-phase check can detect a
|
|
532
|
+
// transition without an effect.
|
|
533
|
+
const [prevIsOpen, setPrevIsOpen] = react.useState(isOpen);
|
|
534
|
+
// Derive `isExiting` from the resolved `isOpen` value's own transitions —
|
|
535
|
+
// the same value used everywhere else in this hook — rather than only from
|
|
536
|
+
// the imperative `close()` / `open()` call sites below. This is what covers
|
|
537
|
+
// a controlled consumer flipping the `isOpen` prop directly (the documented
|
|
538
|
+
// `useDrawer({ isOpen: Boolean(selectedId) })` pattern): `close()` / `open()`
|
|
539
|
+
// alone only ever observe imperative calls, not a controlled prop changing
|
|
540
|
+
// for some other reason (e.g. selecting a different row).
|
|
541
|
+
//
|
|
542
|
+
// This calls `setState` conditionally during the render body — React's
|
|
543
|
+
// "adjust state during render" pattern
|
|
544
|
+
// (https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)
|
|
545
|
+
// — rather than a passive `useEffect` (e.g. `useWatch`). A passive effect
|
|
546
|
+
// only runs one commit AFTER the render where `isOpen` changes, leaving an
|
|
547
|
+
// intermediate commit where `isOpen` has already changed but `isExiting`
|
|
548
|
+
// hasn't yet. `useHold` — `isExiting`'s primary documented consumer — reads
|
|
549
|
+
// both in its own effect and would observe that stale `isExiting: false`
|
|
550
|
+
// there, permanently overwriting its held ref with the already-changed
|
|
551
|
+
// value before `isExiting` ever flips `true`. Calling `setState` here
|
|
552
|
+
// instead makes React detect the change and re-render immediately, before
|
|
553
|
+
// committing or running any effects, so `isOpen` and `isExiting` always
|
|
554
|
+
// land together in the same effective commit that `useHold` observes.
|
|
555
|
+
//
|
|
556
|
+
// `handleExitComplete` (below) is a second, separate way `isExiting` flips
|
|
557
|
+
// back to `false` — the normal close -> fully-exited path. It never fights
|
|
558
|
+
// with this derivation: by the time it fires, `isOpen` has already settled
|
|
559
|
+
// to `false` and `prevIsOpen` has already caught up to it (from the render
|
|
560
|
+
// that flipped `isExiting` to `true` in the first place), so `isOpen ===
|
|
561
|
+
// prevIsOpen` and this block is a no-op on the next render either way.
|
|
562
|
+
if (isOpen !== prevIsOpen) {
|
|
563
|
+
setPrevIsOpen(isOpen);
|
|
564
|
+
setIsExiting(!isOpen); // true when transitioning to closed, false when transitioning to open
|
|
565
|
+
}
|
|
480
566
|
const isPendingCloseRef = react.useRef(false);
|
|
481
567
|
const onCloseRef = react.useRef(onClose);
|
|
482
568
|
const onOpenRef = react.useRef(onOpen);
|
|
@@ -493,6 +579,11 @@ const useDrawer = (props) => {
|
|
|
493
579
|
onCloseRef.current?.(event, reason);
|
|
494
580
|
onOpenChangeRef.current?.(false, event, reason);
|
|
495
581
|
}, []);
|
|
582
|
+
// Fired by `Drawer`'s internal `onExitComplete` prop exactly once per genuine
|
|
583
|
+
// close→fully-exited cycle — the normal path back to `isExiting: false`.
|
|
584
|
+
const handleExitComplete = react.useCallback(() => {
|
|
585
|
+
setIsExiting(false);
|
|
586
|
+
}, []);
|
|
496
587
|
const requestClose = react.useCallback((event, reason) => {
|
|
497
588
|
if (onBeforeCloseRef.current) {
|
|
498
589
|
if (isPendingCloseRef.current) {
|
|
@@ -517,6 +608,12 @@ const useDrawer = (props) => {
|
|
|
517
608
|
onOpenRef.current?.();
|
|
518
609
|
onOpenChangeRef.current?.(true);
|
|
519
610
|
setIsOpen(true);
|
|
611
|
+
// `isExiting` releases automatically here via the render-phase derivation
|
|
612
|
+
// above (the resolved `isOpen` transitioning back to `true`) — a reopen
|
|
613
|
+
// before the exit signal fires releases any hold right away, since the
|
|
614
|
+
// interrupted exit's transitionend resolves as an enter-settle, so
|
|
615
|
+
// `onExitComplete` correctly never fires for that cycle. See
|
|
616
|
+
// `UseDrawerReturnValue.isExiting`.
|
|
520
617
|
}, []);
|
|
521
618
|
const toggle = react.useCallback(() => {
|
|
522
619
|
if (isOpen) {
|
|
@@ -551,7 +648,21 @@ const useDrawer = (props) => {
|
|
|
551
648
|
trapFocus,
|
|
552
649
|
position,
|
|
553
650
|
floatingUi,
|
|
554
|
-
|
|
651
|
+
isExiting,
|
|
652
|
+
onExitComplete: handleExitComplete,
|
|
653
|
+
}), [
|
|
654
|
+
isOpen,
|
|
655
|
+
open,
|
|
656
|
+
close,
|
|
657
|
+
toggle,
|
|
658
|
+
requestClose,
|
|
659
|
+
variant,
|
|
660
|
+
trapFocus,
|
|
661
|
+
position,
|
|
662
|
+
floatingUi,
|
|
663
|
+
isExiting,
|
|
664
|
+
handleExitComplete,
|
|
665
|
+
]);
|
|
555
666
|
};
|
|
556
667
|
|
|
557
668
|
/**
|
package/index.esm.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx, jsxs } from 'react/jsx-runtime';
|
|
|
2
2
|
import { registerTranslations, useNamespaceTranslation } from '@trackunit/i18n-library-translation';
|
|
3
3
|
import { useMergeRefs, FloatingFocusManager, useFloating, useDismiss, useInteractions } from '@floating-ui/react';
|
|
4
4
|
import { useViewportBreakpoints, Portal, IconButton, Icon, MoreMenu } from '@trackunit/react-components';
|
|
5
|
-
import { useRef, useState, useReducer, useEffect,
|
|
5
|
+
import { useRef, useState, useReducer, useEffect, useCallback, useLayoutEffect, useMemo } from 'react';
|
|
6
6
|
import { cva } from '@trackunit/css-class-variance-utilities';
|
|
7
7
|
import { twMerge } from 'tailwind-merge';
|
|
8
8
|
|
|
@@ -306,7 +306,7 @@ const PANEL_FOCUSABLE_SELECTOR = [
|
|
|
306
306
|
* ```
|
|
307
307
|
* @param {DrawerProps} props - The props for the Drawer component
|
|
308
308
|
*/
|
|
309
|
-
const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "data-testid": dataTestId, className, renderInPortal = false, containerClassName, ariaLabel, ariaLabelledBy, }) => {
|
|
309
|
+
const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "data-testid": dataTestId, className, renderInPortal = false, containerClassName, ariaLabel, ariaLabelledBy, onExitComplete, }) => {
|
|
310
310
|
const { isSm } = useViewportBreakpoints();
|
|
311
311
|
const shouldUsePortal = !isSm || renderInPortal;
|
|
312
312
|
const panelRef = useRef(null);
|
|
@@ -314,6 +314,10 @@ const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "d
|
|
|
314
314
|
// the portal (or first paint) has attached the DOM node — refs alone do not.
|
|
315
315
|
const [panelElement, setPanelElement] = useState(null);
|
|
316
316
|
const isOpenRef = useRef(isOpen);
|
|
317
|
+
const onExitCompleteRef = useRef(onExitComplete);
|
|
318
|
+
// Guards against firing onExitComplete twice for the same close cycle if the real
|
|
319
|
+
// transitionend and the headless RAF fallback both resolve the same exit-complete.
|
|
320
|
+
const hasFiredExitCompleteRef = useRef(false);
|
|
317
321
|
const [drawerState, dispatch] = useReducer(drawerAnimationReducer, {
|
|
318
322
|
...INITIAL_DRAWER_ANIMATION_STATE,
|
|
319
323
|
// Mount off-screen when initially open so the enter slide still plays.
|
|
@@ -323,7 +327,22 @@ const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "d
|
|
|
323
327
|
useEffect(() => {
|
|
324
328
|
isOpenRef.current = isOpen;
|
|
325
329
|
}, [isOpen]);
|
|
330
|
+
useEffect(() => {
|
|
331
|
+
onExitCompleteRef.current = onExitComplete;
|
|
332
|
+
}, [onExitComplete]);
|
|
333
|
+
const fireExitComplete = useCallback(() => {
|
|
334
|
+
if (hasFiredExitCompleteRef.current) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
hasFiredExitCompleteRef.current = true;
|
|
338
|
+
onExitCompleteRef.current?.();
|
|
339
|
+
}, []);
|
|
326
340
|
useLayoutEffect(() => {
|
|
341
|
+
if (!isOpen) {
|
|
342
|
+
// A new close cycle is starting — arm the guard so this cycle's genuine
|
|
343
|
+
// exit-complete (real transitionend or headless RAF fallback) can fire.
|
|
344
|
+
hasFiredExitCompleteRef.current = false;
|
|
345
|
+
}
|
|
327
346
|
dispatch({ type: isOpen ? "open" : "close" });
|
|
328
347
|
}, [isOpen]);
|
|
329
348
|
// Defer the open flip until after paint. A single useLayoutEffect flip commits
|
|
@@ -358,9 +377,23 @@ const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "d
|
|
|
358
377
|
const runningAnimations = typeof panelNode.getAnimations === "function" ? panelNode.getAnimations() : [];
|
|
359
378
|
if (runningAnimations.length === 0) {
|
|
360
379
|
dispatch({ type: "transitionEnd" });
|
|
380
|
+
fireExitComplete();
|
|
361
381
|
}
|
|
362
382
|
});
|
|
363
383
|
return () => cancelAnimationFrame(rafId);
|
|
384
|
+
}, [drawerState.mode, drawerState.shouldRender, fireExitComplete]);
|
|
385
|
+
// If transitionend is suppressed while a CSS animation is reported as running
|
|
386
|
+
// (Playwright/iframe), unmount after the motion duration so close cannot stick.
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
if (drawerState.mode !== "closed" || !drawerState.shouldRender) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const timeoutId = window.setTimeout(() => {
|
|
392
|
+
if (!isOpenRef.current) {
|
|
393
|
+
dispatch({ type: "transitionEnd" });
|
|
394
|
+
}
|
|
395
|
+
}, DRAWER_TRANSITION_DURATION_MS + 50);
|
|
396
|
+
return () => window.clearTimeout(timeoutId);
|
|
364
397
|
}, [drawerState.mode, drawerState.shouldRender]);
|
|
365
398
|
// If transitionend is suppressed, settle enter after the motion duration so focus
|
|
366
399
|
// still activates. Do not use a zero-delay rAF here — getAnimations() can be empty
|
|
@@ -403,9 +436,20 @@ const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "d
|
|
|
403
436
|
return;
|
|
404
437
|
}
|
|
405
438
|
if (transitionEvent.propertyName === "transform") {
|
|
439
|
+
// A transform transitionend while mode is still "closed" but isOpen has already
|
|
440
|
+
// flipped back to true is an interrupted exit (the reopen's openVisual is still
|
|
441
|
+
// behind its double-rAF), not an exit-complete. Same guard the headless rAF
|
|
442
|
+
// fallback above already applies.
|
|
443
|
+
if (drawerState.mode === "closed" && isOpenRef.current) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const isGenuineExitComplete = drawerState.mode === "closed";
|
|
406
447
|
dispatch({ type: "transitionEnd" });
|
|
448
|
+
if (isGenuineExitComplete) {
|
|
449
|
+
fireExitComplete();
|
|
450
|
+
}
|
|
407
451
|
}
|
|
408
|
-
}, []);
|
|
452
|
+
}, [drawerState.mode, fireExitComplete]);
|
|
409
453
|
if (!drawerState.shouldRender) {
|
|
410
454
|
return null;
|
|
411
455
|
}
|
|
@@ -475,6 +519,48 @@ const useDrawer = (props) => {
|
|
|
475
519
|
}), [dismissProp]);
|
|
476
520
|
const [internalIsOpen, setIsOpen] = useState(defaultOpen ?? false);
|
|
477
521
|
const isOpen = typeof controlledIsOpen === "boolean" ? controlledIsOpen : internalIsOpen;
|
|
522
|
+
// `false` initially / whenever never opened. Flips to `true` the moment the
|
|
523
|
+
// resolved `isOpen` value above transitions `true -> false`, and back to
|
|
524
|
+
// `false` either when it transitions `false -> true` again or once Drawer's
|
|
525
|
+
// `onExitComplete` signals the panel has genuinely finished exiting
|
|
526
|
+
// (`handleExitComplete`). See `UseDrawerReturnValue.isExiting`.
|
|
527
|
+
const [isExiting, setIsExiting] = useState(false);
|
|
528
|
+
// Tracks the resolved `isOpen` value as of the last render for which
|
|
529
|
+
// `isExiting` was derived below, so the render-phase check can detect a
|
|
530
|
+
// transition without an effect.
|
|
531
|
+
const [prevIsOpen, setPrevIsOpen] = useState(isOpen);
|
|
532
|
+
// Derive `isExiting` from the resolved `isOpen` value's own transitions —
|
|
533
|
+
// the same value used everywhere else in this hook — rather than only from
|
|
534
|
+
// the imperative `close()` / `open()` call sites below. This is what covers
|
|
535
|
+
// a controlled consumer flipping the `isOpen` prop directly (the documented
|
|
536
|
+
// `useDrawer({ isOpen: Boolean(selectedId) })` pattern): `close()` / `open()`
|
|
537
|
+
// alone only ever observe imperative calls, not a controlled prop changing
|
|
538
|
+
// for some other reason (e.g. selecting a different row).
|
|
539
|
+
//
|
|
540
|
+
// This calls `setState` conditionally during the render body — React's
|
|
541
|
+
// "adjust state during render" pattern
|
|
542
|
+
// (https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)
|
|
543
|
+
// — rather than a passive `useEffect` (e.g. `useWatch`). A passive effect
|
|
544
|
+
// only runs one commit AFTER the render where `isOpen` changes, leaving an
|
|
545
|
+
// intermediate commit where `isOpen` has already changed but `isExiting`
|
|
546
|
+
// hasn't yet. `useHold` — `isExiting`'s primary documented consumer — reads
|
|
547
|
+
// both in its own effect and would observe that stale `isExiting: false`
|
|
548
|
+
// there, permanently overwriting its held ref with the already-changed
|
|
549
|
+
// value before `isExiting` ever flips `true`. Calling `setState` here
|
|
550
|
+
// instead makes React detect the change and re-render immediately, before
|
|
551
|
+
// committing or running any effects, so `isOpen` and `isExiting` always
|
|
552
|
+
// land together in the same effective commit that `useHold` observes.
|
|
553
|
+
//
|
|
554
|
+
// `handleExitComplete` (below) is a second, separate way `isExiting` flips
|
|
555
|
+
// back to `false` — the normal close -> fully-exited path. It never fights
|
|
556
|
+
// with this derivation: by the time it fires, `isOpen` has already settled
|
|
557
|
+
// to `false` and `prevIsOpen` has already caught up to it (from the render
|
|
558
|
+
// that flipped `isExiting` to `true` in the first place), so `isOpen ===
|
|
559
|
+
// prevIsOpen` and this block is a no-op on the next render either way.
|
|
560
|
+
if (isOpen !== prevIsOpen) {
|
|
561
|
+
setPrevIsOpen(isOpen);
|
|
562
|
+
setIsExiting(!isOpen); // true when transitioning to closed, false when transitioning to open
|
|
563
|
+
}
|
|
478
564
|
const isPendingCloseRef = useRef(false);
|
|
479
565
|
const onCloseRef = useRef(onClose);
|
|
480
566
|
const onOpenRef = useRef(onOpen);
|
|
@@ -491,6 +577,11 @@ const useDrawer = (props) => {
|
|
|
491
577
|
onCloseRef.current?.(event, reason);
|
|
492
578
|
onOpenChangeRef.current?.(false, event, reason);
|
|
493
579
|
}, []);
|
|
580
|
+
// Fired by `Drawer`'s internal `onExitComplete` prop exactly once per genuine
|
|
581
|
+
// close→fully-exited cycle — the normal path back to `isExiting: false`.
|
|
582
|
+
const handleExitComplete = useCallback(() => {
|
|
583
|
+
setIsExiting(false);
|
|
584
|
+
}, []);
|
|
494
585
|
const requestClose = useCallback((event, reason) => {
|
|
495
586
|
if (onBeforeCloseRef.current) {
|
|
496
587
|
if (isPendingCloseRef.current) {
|
|
@@ -515,6 +606,12 @@ const useDrawer = (props) => {
|
|
|
515
606
|
onOpenRef.current?.();
|
|
516
607
|
onOpenChangeRef.current?.(true);
|
|
517
608
|
setIsOpen(true);
|
|
609
|
+
// `isExiting` releases automatically here via the render-phase derivation
|
|
610
|
+
// above (the resolved `isOpen` transitioning back to `true`) — a reopen
|
|
611
|
+
// before the exit signal fires releases any hold right away, since the
|
|
612
|
+
// interrupted exit's transitionend resolves as an enter-settle, so
|
|
613
|
+
// `onExitComplete` correctly never fires for that cycle. See
|
|
614
|
+
// `UseDrawerReturnValue.isExiting`.
|
|
518
615
|
}, []);
|
|
519
616
|
const toggle = useCallback(() => {
|
|
520
617
|
if (isOpen) {
|
|
@@ -549,7 +646,21 @@ const useDrawer = (props) => {
|
|
|
549
646
|
trapFocus,
|
|
550
647
|
position,
|
|
551
648
|
floatingUi,
|
|
552
|
-
|
|
649
|
+
isExiting,
|
|
650
|
+
onExitComplete: handleExitComplete,
|
|
651
|
+
}), [
|
|
652
|
+
isOpen,
|
|
653
|
+
open,
|
|
654
|
+
close,
|
|
655
|
+
toggle,
|
|
656
|
+
requestClose,
|
|
657
|
+
variant,
|
|
658
|
+
trapFocus,
|
|
659
|
+
position,
|
|
660
|
+
floatingUi,
|
|
661
|
+
isExiting,
|
|
662
|
+
handleExitComplete,
|
|
663
|
+
]);
|
|
553
664
|
};
|
|
554
665
|
|
|
555
666
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entry.js","sourceRoot":"","sources":["../../../../../libs/react/drawer/migrations/entry.ts"],"names":[],"mappings":"","sourcesContent":["export {};\n"]}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-drawer",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.73",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=24.x"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@trackunit/react-components": "2.13.
|
|
10
|
+
"@trackunit/react-components": "2.13.25",
|
|
11
11
|
"@trackunit/css-class-variance-utilities": "2.0.14",
|
|
12
12
|
"@trackunit/i18n-library-translation": "2.4.63",
|
|
13
13
|
"@floating-ui/react": "^0.26.25",
|
|
@@ -73,6 +73,17 @@ export interface DrawerProps extends CommonProps {
|
|
|
73
73
|
* the drawer body renders a visible heading so the two stay in sync.
|
|
74
74
|
*/
|
|
75
75
|
ariaLabelledBy?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Internal signal fired exactly once per genuine open→close→fully-exited cycle,
|
|
78
|
+
* once the panel has actually finished animating (or would have, in headless /
|
|
79
|
+
* test environments where CSS transitions are suppressed) off-screen. Spread from
|
|
80
|
+
* `useDrawer()`, which uses it to flip its `isExiting` state back to `false`.
|
|
81
|
+
*
|
|
82
|
+
* Does not fire for the enter-settle transitionend, on initial mount, while
|
|
83
|
+
* remaining closed, or more than once for the same close cycle even if both the
|
|
84
|
+
* real `transitionend` and the headless RAF fallback resolve it.
|
|
85
|
+
*/
|
|
86
|
+
readonly onExitComplete?: () => void;
|
|
76
87
|
}
|
|
77
88
|
/**
|
|
78
89
|
* Drawers slide in from the left or right edge of the viewport as either a modal
|
|
@@ -135,6 +146,6 @@ export interface DrawerProps extends CommonProps {
|
|
|
135
146
|
* @param {DrawerProps} props - The props for the Drawer component
|
|
136
147
|
*/
|
|
137
148
|
export declare const Drawer: {
|
|
138
|
-
({ isOpen, variant, trapFocus, position, floatingUi, children, "data-testid": dataTestId, className, renderInPortal, containerClassName, ariaLabel, ariaLabelledBy, }: DrawerProps): ReactElement | null;
|
|
149
|
+
({ isOpen, variant, trapFocus, position, floatingUi, children, "data-testid": dataTestId, className, renderInPortal, containerClassName, ariaLabel, ariaLabelledBy, onExitComplete, }: DrawerProps): ReactElement | null;
|
|
139
150
|
displayName: string;
|
|
140
151
|
};
|
|
@@ -98,6 +98,33 @@ export type UseDrawerReturnValue = {
|
|
|
98
98
|
* its `FloatingFocusManager`.
|
|
99
99
|
*/
|
|
100
100
|
readonly floatingUi: DrawerFloatingUiProps;
|
|
101
|
+
/**
|
|
102
|
+
* Whether the drawer is currently in its close→fully-exited cycle. `false`
|
|
103
|
+
* initially and whenever the drawer has never been opened.
|
|
104
|
+
*
|
|
105
|
+
* Derived from the resolved `isOpen` value's own transitions (the same value
|
|
106
|
+
* used everywhere else in the hook — `controlledIsOpen ?? internalIsOpen`),
|
|
107
|
+
* not merely from the imperative `open()` / `close()` call sites. Flips to
|
|
108
|
+
* `true` the moment `isOpen` transitions `true -> false` — whether via
|
|
109
|
+
* `close()` / `requestClose()` committing (after any `onBeforeClose` guard
|
|
110
|
+
* has resolved `true`) or a controlled consumer flipping its `isOpen` prop
|
|
111
|
+
* directly — and back to `false` the moment `isOpen` transitions
|
|
112
|
+
* `false -> true` again (an interrupted exit releases any hold immediately,
|
|
113
|
+
* since the interrupted exit's `transitionend` resolves as an enter-settle
|
|
114
|
+
* and never signals exit-complete), or when the panel has genuinely finished
|
|
115
|
+
* animating off-screen (wired internally via `onExitComplete`, below).
|
|
116
|
+
*
|
|
117
|
+
* Useful for content that wants to survive the exit animation — e.g. via the
|
|
118
|
+
* shared `useHold(value, isExiting)` primitive — by holding its last non-null
|
|
119
|
+
* value while `isExiting` is `true`.
|
|
120
|
+
*/
|
|
121
|
+
readonly isExiting: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Internal signal spread onto `Drawer`'s `onExitComplete` prop. `Drawer` calls
|
|
124
|
+
* this exactly once per genuine close→fully-exited cycle, which flips
|
|
125
|
+
* `isExiting` back to `false`. Not intended for direct use by consumers.
|
|
126
|
+
*/
|
|
127
|
+
readonly onExitComplete: () => void;
|
|
101
128
|
};
|
|
102
129
|
/**
|
|
103
130
|
* Hook for managing Drawer open/close state, dismiss handling, and floating UI wiring.
|