@charcuterie/ui 4.18.1 → 4.18.2
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/Overlay/AnchoredOverlayScope.d.ts +10 -0
- package/dist/Overlay/AnchoredOverlayScope.d.ts.map +1 -0
- package/dist/Overlay/AnchoredOverlayScope.js +15 -0
- package/dist/Overlay/AnchoredOverlayScope.js.map +1 -0
- package/dist/Overlay/OverlayPanel.d.ts.map +1 -1
- package/dist/Overlay/OverlayPanel.js +6 -3
- package/dist/Overlay/OverlayPanel.js.map +1 -1
- package/dist/Overlay/useAnchoredOverlay.d.ts.map +1 -1
- package/dist/Overlay/useAnchoredOverlay.js +15 -3
- package/dist/Overlay/useAnchoredOverlay.js.map +1 -1
- package/package.json +1 -1
- package/src/Dialog/Dialog.mdx +9 -0
- package/src/Dialog/Dialog.stories.tsx +51 -2
- package/src/Dialog/Dialog.test.tsx +59 -0
- package/src/Overlay/AnchoredOverlayScope.ts +42 -0
- package/src/Overlay/OverlayPanel.tsx +14 -3
- package/src/Overlay/useAnchoredOverlay.ts +21 -3
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Anchored children share their enclosing modal's dismissal boundary. */
|
|
2
|
+
type AnchoredOverlayScope = {
|
|
3
|
+
topId: string | null;
|
|
4
|
+
register: (id: string) => void;
|
|
5
|
+
unregister: (id: string) => void;
|
|
6
|
+
};
|
|
7
|
+
export declare const AnchoredOverlayScopeContext: import("react").Context<AnchoredOverlayScope | null>;
|
|
8
|
+
export declare function useAnchoredOverlayScope(): AnchoredOverlayScope;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=AnchoredOverlayScope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnchoredOverlayScope.d.ts","sourceRoot":"","sources":["../../src/Overlay/AnchoredOverlayScope.ts"],"names":[],"mappings":"AAOA,0EAA0E;AAC1E,KAAK,oBAAoB,GAAG;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9B,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC,CAAA;AACD,eAAO,MAAM,2BAA2B,sDACU,CAAA;AAClD,wBAAgB,uBAAuB,IAAI,oBAAoB,CA0B9D"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createContext, useCallback, useMemo, useState, } from "react";
|
|
2
|
+
export const AnchoredOverlayScopeContext = createContext(null);
|
|
3
|
+
export function useAnchoredOverlayScope() {
|
|
4
|
+
const [ids, setIds] = useState([]);
|
|
5
|
+
const register = useCallback((id) => setIds((previous) => previous.includes(id)
|
|
6
|
+
? previous
|
|
7
|
+
: [...previous, id]), []);
|
|
8
|
+
const unregister = useCallback((id) => setIds((previous) => previous.filter((value) => value !== id)), []);
|
|
9
|
+
return useMemo(() => ({
|
|
10
|
+
topId: ids.at(-1) ?? null,
|
|
11
|
+
register,
|
|
12
|
+
unregister,
|
|
13
|
+
}), [ids, register, unregister]);
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=AnchoredOverlayScope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnchoredOverlayScope.js","sourceRoot":"","sources":["../../src/Overlay/AnchoredOverlayScope.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,WAAW,EACX,OAAO,EACP,QAAQ,GACT,MAAM,OAAO,CAAA;AAQd,MAAM,CAAC,MAAM,2BAA2B,GACtC,aAAa,CAA8B,IAAI,CAAC,CAAA;AAClD,MAAM,UAAU,uBAAuB;IACrC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAW,EAAE,CAAC,CAAA;IAC5C,MAAM,QAAQ,GAAG,WAAW,CAC1B,CAAC,EAAU,EAAE,EAAE,CACb,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAClB,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACnB,CAAC,CAAC,QAAQ;QACV,CAAC,CAAC,CAAC,GAAG,QAAQ,EAAE,EAAE,CAAC,CACtB,EACH,EAAE,CACH,CAAA;IACD,MAAM,UAAU,GAAG,WAAW,CAC5B,CAAC,EAAU,EAAE,EAAE,CACb,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAClB,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,EAAE,CAAC,CACzC,EACH,EAAE,CACH,CAAA;IACD,OAAO,OAAO,CACZ,GAAG,EAAE,CAAC,CAAC;QACL,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI;QACzB,QAAQ;QACR,UAAU;KACX,CAAC,EACF,CAAC,GAAG,EAAE,QAAQ,EAAE,UAAU,CAAC,CAC5B,CAAA;AACH,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OverlayPanel.d.ts","sourceRoot":"","sources":["../../src/Overlay/OverlayPanel.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"OverlayPanel.d.ts","sourceRoot":"","sources":["../../src/Overlay/OverlayPanel.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAWjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IAC5C,8CAA8C;IAC9C,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,SAAS,EAAE,OAAO,CAAA;IAClB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,IAAI,CAAC,EAAE,aAAa,GAAG,QAAQ,CAAA;CAChC,CAAA;AAED,eAAO,MAAM,YAAY,GAAI,6IAU1B,iBAAiB,KAAG,SA8LtB,CAAA"}
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { useLatestRef, useUniqueId, } from "@charcuterie/logic";
|
|
3
3
|
import { FloatingFocusManager, FloatingPortal, useDismiss, useFloating, useInteractions, useRole, } from "@floating-ui/react";
|
|
4
4
|
import { useCallback, useEffect } from "react";
|
|
5
|
+
import { AnchoredOverlayScopeContext, useAnchoredOverlayScope, } from "./AnchoredOverlayScope.js";
|
|
5
6
|
import { lockScrollBehind } from "./lockScrollBehind.js";
|
|
6
7
|
import { useOverlayStack } from "./OverlayStack.js";
|
|
7
8
|
import { SharedBackdrop } from "./SharedBackdrop.js";
|
|
@@ -15,6 +16,8 @@ export const OverlayPanel = ({ "aria-label": ariaLabel, "aria-labelledby": ariaL
|
|
|
15
16
|
onCloseRef.current();
|
|
16
17
|
}, [onCloseRef]);
|
|
17
18
|
const isTop = !stack.isProvided || stack.topId === id;
|
|
19
|
+
const anchors = useAnchoredOverlayScope();
|
|
20
|
+
const hasOpenAnchor = anchors.topId !== null;
|
|
18
21
|
const { context, refs } = useFloating({
|
|
19
22
|
open: isVisible,
|
|
20
23
|
onOpenChange: (isNextVisible) => {
|
|
@@ -25,13 +28,13 @@ export const OverlayPanel = ({ "aria-label": ariaLabel, "aria-labelledby": ariaL
|
|
|
25
28
|
});
|
|
26
29
|
const { getFloatingProps } = useInteractions([
|
|
27
30
|
useDismiss(context, {
|
|
28
|
-
escapeKey: isDismissable && isTop,
|
|
31
|
+
escapeKey: isDismissable && isTop && !hasOpenAnchor,
|
|
29
32
|
// The top's outside press is the backdrop click. A per-panel
|
|
30
33
|
// `outsidePress` on a *lower* modal would close it too, so it is
|
|
31
34
|
// scoped to the top — which is also why the scrim needs no click
|
|
32
35
|
// handler of its own.
|
|
33
36
|
outsidePress: (event) => {
|
|
34
|
-
if (!isDismissable || !isTop)
|
|
37
|
+
if (!isDismissable || !isTop || hasOpenAnchor)
|
|
35
38
|
return false;
|
|
36
39
|
/*
|
|
37
40
|
* ⚠️ A press on a toast is NOT an outside press.
|
|
@@ -123,6 +126,6 @@ export const OverlayPanel = ({ "aria-label": ariaLabel, "aria-labelledby": ariaL
|
|
|
123
126
|
// but the top, so a lower modal is unreachable across the
|
|
124
127
|
// portal boundary even where `FloatingFocusManager`'s
|
|
125
128
|
// outside-hiding does not reach a sibling portal.
|
|
126
|
-
inert: isTop ? undefined : true, ref: refs.setFloating, role: role, children: children }) }) }) })] }));
|
|
129
|
+
inert: isTop ? undefined : true, ref: refs.setFloating, role: role, children: _jsx(AnchoredOverlayScopeContext.Provider, { value: anchors, children: children }) }) }) }) })] }));
|
|
127
130
|
};
|
|
128
131
|
//# sourceMappingURL=OverlayPanel.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OverlayPanel.js","sourceRoot":"","sources":["../../src/Overlay/OverlayPanel.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,YAAY,EACZ,WAAW,GACZ,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,oBAAoB,EACpB,cAAc,EACd,UAAU,EACV,WAAW,EACX,eAAe,EACf,OAAO,GACR,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAE9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAoB,CAAA;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAsB,CAAA;AA6DrD,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,EAC3B,YAAY,EAAE,SAAS,EACvB,iBAAiB,EAAE,cAAc,EACjC,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,aAAa,GAAG,IAAI,EACpB,SAAS,EACT,OAAO,EACP,IAAI,GAAG,QAAQ,GACG,EAAa,EAAE;IACjC,MAAM,KAAK,GAAG,eAAe,EAAE,CAAA;IAE/B,MAAM,EAAE,GAAG,WAAW,EAAE,CAAA;IAExB,oEAAoE;IACpE,+DAA+D;IAC/D,MAAM,UAAU,GAAG,YAAY,CAAC,OAAO,CAAC,CAAA;IAExC,MAAM,aAAa,GAAG,WAAW,CAAC,GAAG,EAAE;QACrC,UAAU,CAAC,OAAO,EAAE,CAAA;IACtB,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAA;IAEhB,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,KAAK,KAAK,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"OverlayPanel.js","sourceRoot":"","sources":["../../src/Overlay/OverlayPanel.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,YAAY,EACZ,WAAW,GACZ,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,oBAAoB,EACpB,cAAc,EACd,UAAU,EACV,WAAW,EACX,eAAe,EACf,OAAO,GACR,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAE9C,OAAO,EACL,2BAA2B,EAC3B,uBAAuB,GACxB,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAoB,CAAA;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAsB,CAAA;AA6DrD,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,EAC3B,YAAY,EAAE,SAAS,EACvB,iBAAiB,EAAE,cAAc,EACjC,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,aAAa,GAAG,IAAI,EACpB,SAAS,EACT,OAAO,EACP,IAAI,GAAG,QAAQ,GACG,EAAa,EAAE;IACjC,MAAM,KAAK,GAAG,eAAe,EAAE,CAAA;IAE/B,MAAM,EAAE,GAAG,WAAW,EAAE,CAAA;IAExB,oEAAoE;IACpE,+DAA+D;IAC/D,MAAM,UAAU,GAAG,YAAY,CAAC,OAAO,CAAC,CAAA;IAExC,MAAM,aAAa,GAAG,WAAW,CAAC,GAAG,EAAE;QACrC,UAAU,CAAC,OAAO,EAAE,CAAA;IACtB,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAA;IAEhB,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,KAAK,KAAK,EAAE,CAAA;IACrD,MAAM,OAAO,GAAG,uBAAuB,EAAE,CAAA;IACzC,MAAM,aAAa,GAAG,OAAO,CAAC,KAAK,KAAK,IAAI,CAAA;IAE5C,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,WAAW,CAAC;QACpC,IAAI,EAAE,SAAS;QACf,YAAY,EAAE,CAAC,aAAa,EAAE,EAAE;YAC9B,IAAI,CAAC,aAAa,EAAE,CAAC;gBACnB,OAAO,EAAE,CAAA;YACX,CAAC;QACH,CAAC;KACF,CAAC,CAAA;IAEF,MAAM,EAAE,gBAAgB,EAAE,GAAG,eAAe,CAAC;QAC3C,UAAU,CAAC,OAAO,EAAE;YAClB,SAAS,EAAE,aAAa,IAAI,KAAK,IAAI,CAAC,aAAa;YACnD,6DAA6D;YAC7D,iEAAiE;YACjE,iEAAiE;YACjE,sBAAsB;YACtB,YAAY,EAAE,CAAC,KAAK,EAAE,EAAE;gBACtB,IAAI,CAAC,aAAa,IAAI,CAAC,KAAK,IAAI,aAAa;oBAC3C,OAAO,KAAK,CAAA;gBACd;;;;;;;;;;;;;;;;;;;;;;;mBAuBG;gBACH,MAAM,MAAM,GAAG,KAAK,CAAC,MAAwB,CAAA;gBAC7C,OAAO,CACL,MAAM,EAAE,OAAO,EAAE,CACf,iCAAiC,CAClC,IAAI,IAAI,CACV,CAAA;YACH,CAAC;SACF,CAAC;QACF,OAAO,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC;KAC3B,CAAC,CAAA;IAEF,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,KAAK,CAAA;IAEtC;;;;OAIG;IACH,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CACzB,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC,EAC5B,CAAC,CACF,CAAA;IAED,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAM;QACR,CAAC;QAED,QAAQ,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAA;QAExC,OAAO,GAAG,EAAE;YACV,UAAU,CAAC,EAAE,CAAC,CAAA;QAChB,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,UAAU,CAAC,CAAC,CAAA;IAExD,8DAA8D;IAC9D,mEAAmE;IACnE,uCAAuC;IACvC,SAAS,CACP,GAAG,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,EAClD,CAAC,SAAS,CAAC,CACZ,CAAA;IAED,SAAS,CAAC,GAAG,EAAE;QACb,IACE,SAAS;YACT,SAAS,KAAK,SAAS;YACvB,cAAc,KAAK,SAAS,EAC5B,CAAC;YACD,OAAO,CAAC,IAAI,CACV,2KAA2K,CAC5K,CAAA;QACH,CAAC;IACH,CAAC,EAAE,CAAC,SAAS,EAAE,cAAc,EAAE,SAAS,CAAC,CAAC,CAAA;IAE1C,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,IAAI,CAAA;IACb,CAAC;IAED,OAAO,CACL,8BAGG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACzB,KAAC,cAAc,IAAC,SAAS,SAAG,CAC7B,EAED,KAAC,cAAc,cACb,cACE,SAAS,EAAC,gDAAgD;oBAC1D;;;;;;;;;;;;;uBAaG;oBACH,KAAK,EAAE;wBACL,MAAM,EAAE,6BAA6B,UAAU,GAAG;qBACnD,YAED,KAAC,oBAAoB,IACnB,OAAO,EAAE,OAAO;wBAChB,yDAAyD;wBACzD,0DAA0D;wBAC1D,OAAO;wBACP,YAAY,EAAE,YAAY,IAAI,SAAS;wBACvC,qDAAqD;wBACrD,mDAAmD;wBACnD,QAAQ,EAAE,CAAC,KAAK,EAChB,KAAK,kBAEL,iBACM,gBAAgB,EAAE,EAOpB,YAAY,EAAE,SAAS;4BACvB,iBAAiB,EAAE,cAAc,EAEnC,SAAS,EAAE,SAAS;4BACpB,0DAA0D;4BAC1D,0DAA0D;4BAC1D,sDAAsD;4BACtD,kDAAkD;4BAClD,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,EAC/B,GAAG,EAAE,IAAI,CAAC,WAAW,EACrB,IAAI,EAAE,IAAI,YAEV,KAAC,2BAA2B,CAAC,QAAQ,IACnC,KAAK,EAAE,OAAO,YAEb,QAAQ,GAC4B,GACnC,GACe,GACnB,GACS,IAChB,CACJ,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAnchoredOverlay.d.ts","sourceRoot":"","sources":["../../src/Overlay/useAnchoredOverlay.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,SAAS,EACT,cAAc,EACf,MAAM,oBAAoB,CAAA;AAY3B,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"useAnchoredOverlay.d.ts","sourceRoot":"","sources":["../../src/Overlay/useAnchoredOverlay.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,SAAS,EACT,cAAc,EACf,MAAM,oBAAoB,CAAA;AAY3B,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAQpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,MAAM,MAAM,mBAAmB,GAC3B,QAAQ,GACR,SAAS,GACT,MAAM,CAAA;AAEV;;;;;;;;;;GAUG;AACH,MAAM,MAAM,qBAAqB,GAC7B,WAAW,GACX,cAAc,CAAA;AAElB,MAAM,MAAM,yBAAyB,GAAG;IACtC;;;;;;;;;;OAUG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAA;IACnD;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B;;;;;;;;;OASG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,yBAAyB,CAAC,EAAE,OAAO,CAAA;IACnC,+DAA+D;IAC/D,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,SAAS,EAAE,OAAO,CAAA;IAClB,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,0CAA0C;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,IAAI,EAAE,mBAAmB,CAAA;IACzB;;;OAGG;IACH,OAAO,CAAC,EAAE,YAAY,CAAA;CACvB,CAAA;AAQD,eAAO,MAAM,kBAAkB,GAAI,8MAehC,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;CAyM3B,CAAA"}
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import { useClonedChild, useUniqueId, } from "@charcuterie/logic";
|
|
2
2
|
import { autoUpdate, flip, offset, shift, size, useDismiss, useFloating, useInteractions, useRole, } from "@floating-ui/react";
|
|
3
|
-
import { createElement, useLayoutEffect } from "react";
|
|
3
|
+
import { createElement, useContext, useLayoutEffect, } from "react";
|
|
4
|
+
import { AnchoredOverlayScopeContext } from "./AnchoredOverlayScope.js";
|
|
4
5
|
// `useClonedChild` insists on exactly one element, and hooks cannot be
|
|
5
6
|
// called conditionally — so in `anchorRef` mode we still call it, on this
|
|
6
7
|
// throwaway, and discard the result. It is never rendered and never
|
|
7
8
|
// receives the floating reference (that goes to `anchorRef`).
|
|
8
9
|
const PLACEHOLDER_TRIGGER = createElement("span");
|
|
9
10
|
export const useAnchoredOverlay = ({ anchorRef, isEscapeDismissable = true, isHeightClamped = false, isOutsidePressDismissable = true, isTriggerWidthMatched = false, isVisible, isWidthClamped = false, maxHeightPx, maxWidthPx, offsetValue = 8, onDismiss, placement = "bottom-start", role, trigger, }) => {
|
|
11
|
+
const scope = useContext(AnchoredOverlayScopeContext);
|
|
12
|
+
const anchorId = useUniqueId();
|
|
13
|
+
const register = scope?.register;
|
|
14
|
+
const unregister = scope?.unregister;
|
|
15
|
+
useLayoutEffect(() => {
|
|
16
|
+
if (!isVisible || !register || !unregister)
|
|
17
|
+
return;
|
|
18
|
+
register(anchorId);
|
|
19
|
+
return () => unregister(anchorId);
|
|
20
|
+
}, [isVisible, anchorId, register, unregister]);
|
|
21
|
+
const isTopAnchor = scope === null || scope.topId === anchorId;
|
|
10
22
|
const middleware = [
|
|
11
23
|
offset(offsetValue),
|
|
12
24
|
flip(),
|
|
@@ -58,8 +70,8 @@ export const useAnchoredOverlay = ({ anchorRef, isEscapeDismissable = true, isHe
|
|
|
58
70
|
// decision raised and this answers.
|
|
59
71
|
const { getFloatingProps, getReferenceProps } = useInteractions([
|
|
60
72
|
useDismiss(context, {
|
|
61
|
-
escapeKey: isEscapeDismissable,
|
|
62
|
-
outsidePress: isOutsidePressDismissable,
|
|
73
|
+
escapeKey: isEscapeDismissable && isTopAnchor,
|
|
74
|
+
outsidePress: isOutsidePressDismissable && isTopAnchor,
|
|
63
75
|
}),
|
|
64
76
|
useRole(context, { role }),
|
|
65
77
|
]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAnchoredOverlay.js","sourceRoot":"","sources":["../../src/Overlay/useAnchoredOverlay.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,WAAW,GACZ,MAAM,oBAAoB,CAAA;AAK3B,OAAO,EACL,UAAU,EACV,IAAI,EACJ,MAAM,EACN,KAAK,EACL,IAAI,EACJ,UAAU,EACV,WAAW,EACX,eAAe,EACf,OAAO,GACR,MAAM,oBAAoB,CAAA;AAE3B,OAAO,
|
|
1
|
+
{"version":3,"file":"useAnchoredOverlay.js","sourceRoot":"","sources":["../../src/Overlay/useAnchoredOverlay.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/Dialog/Dialog.mdx
CHANGED
|
@@ -54,6 +54,15 @@ differential — the dialog that ignores Escape is then closed by its own button
|
|
|
54
54
|
|
|
55
55
|
<Canvas of={DialogStories.AllStates} />
|
|
56
56
|
|
|
57
|
+
## Pickers inside a draft
|
|
58
|
+
|
|
59
|
+
An open picker, menu, or popover owns the first Escape or outside press inside a
|
|
60
|
+
modal. Dismissing that child keeps the enclosing dialog and its unsaved fields open.
|
|
61
|
+
After the child closes, Escape dismisses the dialog normally. Selecting a picker
|
|
62
|
+
option also keeps the draft open and returns focus to its trigger.
|
|
63
|
+
|
|
64
|
+
<Canvas of={DialogStories.PickerDraft} />
|
|
65
|
+
|
|
57
66
|
## Stacking, and the one shared scrim
|
|
58
67
|
|
|
59
68
|
A lone dialog needs no setup. Stack one over another and an `OverlayStackProvider` at the
|
|
@@ -2,12 +2,13 @@ import { useVisibility } from "@charcuterie/logic"
|
|
|
2
2
|
import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters"
|
|
3
3
|
import type { Meta, StoryObj } from "@storybook/react"
|
|
4
4
|
import type { ReactNode } from "react"
|
|
5
|
-
import { useRef } from "react"
|
|
6
|
-
|
|
5
|
+
import { useRef, useState } from "react"
|
|
7
6
|
import { toStoryChoice } from "../argTypes.storyHelpers.ts"
|
|
8
7
|
import { Button } from "../Button/Button.tsx"
|
|
9
8
|
import { StoryRow } from "../board.storyHelpers.tsx"
|
|
9
|
+
import { Field } from "../Field/Field.tsx"
|
|
10
10
|
import { OverlayStackProvider } from "../Overlay/OverlayStack.tsx"
|
|
11
|
+
import { Picker } from "../Picker/Picker.tsx"
|
|
11
12
|
import type { DialogSize } from "./Dialog.tsx"
|
|
12
13
|
import { Dialog } from "./Dialog.tsx"
|
|
13
14
|
|
|
@@ -445,3 +446,51 @@ export const Interactive: Story = {
|
|
|
445
446
|
/>
|
|
446
447
|
),
|
|
447
448
|
}
|
|
449
|
+
|
|
450
|
+
const PickerDraftDemo = () => {
|
|
451
|
+
const [isVisible, setIsVisible] = useState(true)
|
|
452
|
+
const [draft, setDraft] = useState(
|
|
453
|
+
"Keep the unsaved draft",
|
|
454
|
+
)
|
|
455
|
+
const [value, setValue] = useState("")
|
|
456
|
+
return (
|
|
457
|
+
<div className="min-h-screen">
|
|
458
|
+
<Button onClick={() => setIsVisible(true)}>
|
|
459
|
+
Open draft
|
|
460
|
+
</Button>
|
|
461
|
+
<Dialog
|
|
462
|
+
heading="Edit a draft"
|
|
463
|
+
isVisible={isVisible}
|
|
464
|
+
onClose={() => setIsVisible(false)}
|
|
465
|
+
>
|
|
466
|
+
<Field label="Draft name">
|
|
467
|
+
<input
|
|
468
|
+
value={draft}
|
|
469
|
+
onChange={(event) =>
|
|
470
|
+
setDraft(event.currentTarget.value)
|
|
471
|
+
}
|
|
472
|
+
/>
|
|
473
|
+
</Field>
|
|
474
|
+
<Picker
|
|
475
|
+
label="Draft parent"
|
|
476
|
+
value={value}
|
|
477
|
+
options={[
|
|
478
|
+
{ value: "", label: "No parent" },
|
|
479
|
+
{ value: "other", label: "Another draft" },
|
|
480
|
+
]}
|
|
481
|
+
onChange={setValue}
|
|
482
|
+
/>
|
|
483
|
+
</Dialog>
|
|
484
|
+
</div>
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Escape dismisses the picker's portal before its enclosing dialog. */
|
|
489
|
+
export const PickerDraft: Story = {
|
|
490
|
+
args: {
|
|
491
|
+
heading: "Edit a draft",
|
|
492
|
+
isVisible: true,
|
|
493
|
+
onClose: () => {},
|
|
494
|
+
},
|
|
495
|
+
render: () => <PickerDraftDemo />,
|
|
496
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import * as stories from "./Dialog.stories.tsx"
|
|
12
12
|
|
|
13
13
|
const {
|
|
14
|
+
PickerDraft,
|
|
14
15
|
AllStates,
|
|
15
16
|
AllVariants,
|
|
16
17
|
Playground,
|
|
@@ -368,3 +369,61 @@ test("initialFocus puts the caret in the field, not on Close", async () => {
|
|
|
368
369
|
|
|
369
370
|
await expect(field).toHaveValue("Sharpen the chisels")
|
|
370
371
|
})
|
|
372
|
+
|
|
373
|
+
test("a picker cancels its selection without closing the enclosing draft", async () => {
|
|
374
|
+
const { body } = await mountStory(PickerDraft)
|
|
375
|
+
const input = expectAgentDrivable(body, {
|
|
376
|
+
role: "textbox",
|
|
377
|
+
name: "Draft name",
|
|
378
|
+
})
|
|
379
|
+
await userEvent.clear(input)
|
|
380
|
+
await userEvent.type(input, "An unsaved change")
|
|
381
|
+
const trigger = body.getByRole("button", {
|
|
382
|
+
name: "Draft parent: No parent",
|
|
383
|
+
})
|
|
384
|
+
await userEvent.click(trigger)
|
|
385
|
+
const option = body.getByRole("option", {
|
|
386
|
+
name: "No parent",
|
|
387
|
+
})
|
|
388
|
+
await expect(option).toHaveFocus()
|
|
389
|
+
await userEvent.keyboard("{Escape}")
|
|
390
|
+
await expect(
|
|
391
|
+
body.getByRole("dialog", { name: "Edit a draft" }),
|
|
392
|
+
).toBeVisible()
|
|
393
|
+
await expect(input).toHaveValue("An unsaved change")
|
|
394
|
+
await expect(trigger).toHaveFocus()
|
|
395
|
+
await expect(
|
|
396
|
+
body.queryByRole("listbox"),
|
|
397
|
+
).not.toBeInTheDocument()
|
|
398
|
+
// A press elsewhere in the draft also dismisses only the picker.
|
|
399
|
+
await userEvent.click(trigger)
|
|
400
|
+
await expect(
|
|
401
|
+
body.getByRole("option", { name: "No parent" }),
|
|
402
|
+
).toHaveFocus()
|
|
403
|
+
await userEvent.click(input)
|
|
404
|
+
await expect(
|
|
405
|
+
body.queryByRole("listbox"),
|
|
406
|
+
).not.toBeInTheDocument()
|
|
407
|
+
await expect(
|
|
408
|
+
body.getByRole("dialog", { name: "Edit a draft" }),
|
|
409
|
+
).toBeVisible()
|
|
410
|
+
await expect(input).toHaveValue("An unsaved change")
|
|
411
|
+
await userEvent.click(trigger)
|
|
412
|
+
await userEvent.click(
|
|
413
|
+
body.getByRole("option", { name: "Another draft" }),
|
|
414
|
+
)
|
|
415
|
+
await expect(
|
|
416
|
+
body.getByRole("dialog", { name: "Edit a draft" }),
|
|
417
|
+
).toBeVisible()
|
|
418
|
+
await expect(
|
|
419
|
+
body.getByRole("button", {
|
|
420
|
+
name: "Draft parent: Another draft",
|
|
421
|
+
}),
|
|
422
|
+
).toHaveFocus()
|
|
423
|
+
await userEvent.keyboard("{Escape}")
|
|
424
|
+
await waitFor(() =>
|
|
425
|
+
expect(
|
|
426
|
+
body.queryByRole("dialog", { name: "Edit a draft" }),
|
|
427
|
+
).not.toBeInTheDocument(),
|
|
428
|
+
)
|
|
429
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useMemo,
|
|
5
|
+
useState,
|
|
6
|
+
} from "react"
|
|
7
|
+
|
|
8
|
+
/** Anchored children share their enclosing modal's dismissal boundary. */
|
|
9
|
+
type AnchoredOverlayScope = {
|
|
10
|
+
topId: string | null
|
|
11
|
+
register: (id: string) => void
|
|
12
|
+
unregister: (id: string) => void
|
|
13
|
+
}
|
|
14
|
+
export const AnchoredOverlayScopeContext =
|
|
15
|
+
createContext<AnchoredOverlayScope | null>(null)
|
|
16
|
+
export function useAnchoredOverlayScope(): AnchoredOverlayScope {
|
|
17
|
+
const [ids, setIds] = useState<string[]>([])
|
|
18
|
+
const register = useCallback(
|
|
19
|
+
(id: string) =>
|
|
20
|
+
setIds((previous) =>
|
|
21
|
+
previous.includes(id)
|
|
22
|
+
? previous
|
|
23
|
+
: [...previous, id],
|
|
24
|
+
),
|
|
25
|
+
[],
|
|
26
|
+
)
|
|
27
|
+
const unregister = useCallback(
|
|
28
|
+
(id: string) =>
|
|
29
|
+
setIds((previous) =>
|
|
30
|
+
previous.filter((value) => value !== id),
|
|
31
|
+
),
|
|
32
|
+
[],
|
|
33
|
+
)
|
|
34
|
+
return useMemo(
|
|
35
|
+
() => ({
|
|
36
|
+
topId: ids.at(-1) ?? null,
|
|
37
|
+
register,
|
|
38
|
+
unregister,
|
|
39
|
+
}),
|
|
40
|
+
[ids, register, unregister],
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -13,6 +13,10 @@ import {
|
|
|
13
13
|
import type { ReactNode, RefObject } from "react"
|
|
14
14
|
import { useCallback, useEffect } from "react"
|
|
15
15
|
|
|
16
|
+
import {
|
|
17
|
+
AnchoredOverlayScopeContext,
|
|
18
|
+
useAnchoredOverlayScope,
|
|
19
|
+
} from "./AnchoredOverlayScope.ts"
|
|
16
20
|
import { lockScrollBehind } from "./lockScrollBehind.ts"
|
|
17
21
|
import { useOverlayStack } from "./OverlayStack.tsx"
|
|
18
22
|
import { SharedBackdrop } from "./SharedBackdrop.tsx"
|
|
@@ -100,6 +104,8 @@ export const OverlayPanel = ({
|
|
|
100
104
|
}, [onCloseRef])
|
|
101
105
|
|
|
102
106
|
const isTop = !stack.isProvided || stack.topId === id
|
|
107
|
+
const anchors = useAnchoredOverlayScope()
|
|
108
|
+
const hasOpenAnchor = anchors.topId !== null
|
|
103
109
|
|
|
104
110
|
const { context, refs } = useFloating({
|
|
105
111
|
open: isVisible,
|
|
@@ -112,13 +118,14 @@ export const OverlayPanel = ({
|
|
|
112
118
|
|
|
113
119
|
const { getFloatingProps } = useInteractions([
|
|
114
120
|
useDismiss(context, {
|
|
115
|
-
escapeKey: isDismissable && isTop,
|
|
121
|
+
escapeKey: isDismissable && isTop && !hasOpenAnchor,
|
|
116
122
|
// The top's outside press is the backdrop click. A per-panel
|
|
117
123
|
// `outsidePress` on a *lower* modal would close it too, so it is
|
|
118
124
|
// scoped to the top — which is also why the scrim needs no click
|
|
119
125
|
// handler of its own.
|
|
120
126
|
outsidePress: (event) => {
|
|
121
|
-
if (!isDismissable || !isTop
|
|
127
|
+
if (!isDismissable || !isTop || hasOpenAnchor)
|
|
128
|
+
return false
|
|
122
129
|
/*
|
|
123
130
|
* ⚠️ A press on a toast is NOT an outside press.
|
|
124
131
|
*
|
|
@@ -262,7 +269,11 @@ export const OverlayPanel = ({
|
|
|
262
269
|
ref={refs.setFloating}
|
|
263
270
|
role={role}
|
|
264
271
|
>
|
|
265
|
-
|
|
272
|
+
<AnchoredOverlayScopeContext.Provider
|
|
273
|
+
value={anchors}
|
|
274
|
+
>
|
|
275
|
+
{children}
|
|
276
|
+
</AnchoredOverlayScopeContext.Provider>
|
|
266
277
|
</div>
|
|
267
278
|
</FloatingFocusManager>
|
|
268
279
|
</div>
|
|
@@ -18,7 +18,12 @@ import {
|
|
|
18
18
|
useRole,
|
|
19
19
|
} from "@floating-ui/react"
|
|
20
20
|
import type { ReactElement, RefObject } from "react"
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
createElement,
|
|
23
|
+
useContext,
|
|
24
|
+
useLayoutEffect,
|
|
25
|
+
} from "react"
|
|
26
|
+
import { AnchoredOverlayScopeContext } from "./AnchoredOverlayScope.ts"
|
|
22
27
|
|
|
23
28
|
/**
|
|
24
29
|
* The floating-ui block every anchored overlay — `Popover`, `Menu`,
|
|
@@ -178,6 +183,18 @@ export const useAnchoredOverlay = ({
|
|
|
178
183
|
role,
|
|
179
184
|
trigger,
|
|
180
185
|
}: UseAnchoredOverlayOptions) => {
|
|
186
|
+
const scope = useContext(AnchoredOverlayScopeContext)
|
|
187
|
+
const anchorId = useUniqueId()
|
|
188
|
+
const register = scope?.register
|
|
189
|
+
const unregister = scope?.unregister
|
|
190
|
+
useLayoutEffect(() => {
|
|
191
|
+
if (!isVisible || !register || !unregister) return
|
|
192
|
+
register(anchorId)
|
|
193
|
+
return () => unregister(anchorId)
|
|
194
|
+
}, [isVisible, anchorId, register, unregister])
|
|
195
|
+
const isTopAnchor =
|
|
196
|
+
scope === null || scope.topId === anchorId
|
|
197
|
+
|
|
181
198
|
const middleware = [
|
|
182
199
|
offset(offsetValue),
|
|
183
200
|
flip(),
|
|
@@ -249,8 +266,9 @@ export const useAnchoredOverlay = ({
|
|
|
249
266
|
const { getFloatingProps, getReferenceProps } =
|
|
250
267
|
useInteractions([
|
|
251
268
|
useDismiss(context, {
|
|
252
|
-
escapeKey: isEscapeDismissable,
|
|
253
|
-
outsidePress:
|
|
269
|
+
escapeKey: isEscapeDismissable && isTopAnchor,
|
|
270
|
+
outsidePress:
|
|
271
|
+
isOutsidePressDismissable && isTopAnchor,
|
|
254
272
|
}),
|
|
255
273
|
useRole(context, { role }),
|
|
256
274
|
])
|