@trackunit/react-drawer 2.6.72 → 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 +102 -4
- package/index.esm.js +103 -5
- 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,10 +379,11 @@ 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);
|
|
366
|
-
}, [drawerState.mode, drawerState.shouldRender]);
|
|
386
|
+
}, [drawerState.mode, drawerState.shouldRender, fireExitComplete]);
|
|
367
387
|
// If transitionend is suppressed while a CSS animation is reported as running
|
|
368
388
|
// (Playwright/iframe), unmount after the motion duration so close cannot stick.
|
|
369
389
|
react.useEffect(() => {
|
|
@@ -418,9 +438,20 @@ const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "d
|
|
|
418
438
|
return;
|
|
419
439
|
}
|
|
420
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";
|
|
421
449
|
dispatch({ type: "transitionEnd" });
|
|
450
|
+
if (isGenuineExitComplete) {
|
|
451
|
+
fireExitComplete();
|
|
452
|
+
}
|
|
422
453
|
}
|
|
423
|
-
}, []);
|
|
454
|
+
}, [drawerState.mode, fireExitComplete]);
|
|
424
455
|
if (!drawerState.shouldRender) {
|
|
425
456
|
return null;
|
|
426
457
|
}
|
|
@@ -490,6 +521,48 @@ const useDrawer = (props) => {
|
|
|
490
521
|
}), [dismissProp]);
|
|
491
522
|
const [internalIsOpen, setIsOpen] = react.useState(defaultOpen ?? false);
|
|
492
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
|
+
}
|
|
493
566
|
const isPendingCloseRef = react.useRef(false);
|
|
494
567
|
const onCloseRef = react.useRef(onClose);
|
|
495
568
|
const onOpenRef = react.useRef(onOpen);
|
|
@@ -506,6 +579,11 @@ const useDrawer = (props) => {
|
|
|
506
579
|
onCloseRef.current?.(event, reason);
|
|
507
580
|
onOpenChangeRef.current?.(false, event, reason);
|
|
508
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
|
+
}, []);
|
|
509
587
|
const requestClose = react.useCallback((event, reason) => {
|
|
510
588
|
if (onBeforeCloseRef.current) {
|
|
511
589
|
if (isPendingCloseRef.current) {
|
|
@@ -530,6 +608,12 @@ const useDrawer = (props) => {
|
|
|
530
608
|
onOpenRef.current?.();
|
|
531
609
|
onOpenChangeRef.current?.(true);
|
|
532
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`.
|
|
533
617
|
}, []);
|
|
534
618
|
const toggle = react.useCallback(() => {
|
|
535
619
|
if (isOpen) {
|
|
@@ -564,7 +648,21 @@ const useDrawer = (props) => {
|
|
|
564
648
|
trapFocus,
|
|
565
649
|
position,
|
|
566
650
|
floatingUi,
|
|
567
|
-
|
|
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
|
+
]);
|
|
568
666
|
};
|
|
569
667
|
|
|
570
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,10 +377,11 @@ 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);
|
|
364
|
-
}, [drawerState.mode, drawerState.shouldRender]);
|
|
384
|
+
}, [drawerState.mode, drawerState.shouldRender, fireExitComplete]);
|
|
365
385
|
// If transitionend is suppressed while a CSS animation is reported as running
|
|
366
386
|
// (Playwright/iframe), unmount after the motion duration so close cannot stick.
|
|
367
387
|
useEffect(() => {
|
|
@@ -416,9 +436,20 @@ const Drawer = ({ isOpen, variant, trapFocus, position, floatingUi, children, "d
|
|
|
416
436
|
return;
|
|
417
437
|
}
|
|
418
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";
|
|
419
447
|
dispatch({ type: "transitionEnd" });
|
|
448
|
+
if (isGenuineExitComplete) {
|
|
449
|
+
fireExitComplete();
|
|
450
|
+
}
|
|
420
451
|
}
|
|
421
|
-
}, []);
|
|
452
|
+
}, [drawerState.mode, fireExitComplete]);
|
|
422
453
|
if (!drawerState.shouldRender) {
|
|
423
454
|
return null;
|
|
424
455
|
}
|
|
@@ -488,6 +519,48 @@ const useDrawer = (props) => {
|
|
|
488
519
|
}), [dismissProp]);
|
|
489
520
|
const [internalIsOpen, setIsOpen] = useState(defaultOpen ?? false);
|
|
490
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
|
+
}
|
|
491
564
|
const isPendingCloseRef = useRef(false);
|
|
492
565
|
const onCloseRef = useRef(onClose);
|
|
493
566
|
const onOpenRef = useRef(onOpen);
|
|
@@ -504,6 +577,11 @@ const useDrawer = (props) => {
|
|
|
504
577
|
onCloseRef.current?.(event, reason);
|
|
505
578
|
onOpenChangeRef.current?.(false, event, reason);
|
|
506
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
|
+
}, []);
|
|
507
585
|
const requestClose = useCallback((event, reason) => {
|
|
508
586
|
if (onBeforeCloseRef.current) {
|
|
509
587
|
if (isPendingCloseRef.current) {
|
|
@@ -528,6 +606,12 @@ const useDrawer = (props) => {
|
|
|
528
606
|
onOpenRef.current?.();
|
|
529
607
|
onOpenChangeRef.current?.(true);
|
|
530
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`.
|
|
531
615
|
}, []);
|
|
532
616
|
const toggle = useCallback(() => {
|
|
533
617
|
if (isOpen) {
|
|
@@ -562,7 +646,21 @@ const useDrawer = (props) => {
|
|
|
562
646
|
trapFocus,
|
|
563
647
|
position,
|
|
564
648
|
floatingUi,
|
|
565
|
-
|
|
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
|
+
]);
|
|
566
664
|
};
|
|
567
665
|
|
|
568
666
|
/**
|
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.
|