@charcuterie/ui 4.18.0 → 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/dist/Tabs/PanelTabs.d.ts.map +1 -1
- package/dist/Tabs/PanelTabs.js +6 -1
- package/dist/Tabs/PanelTabs.js.map +1 -1
- package/dist/Tabs/RoutedTabs.d.ts.map +1 -1
- package/dist/Tabs/RoutedTabs.js +3 -1
- package/dist/Tabs/RoutedTabs.js.map +1 -1
- package/dist/Tabs/useRevealCurrentTab.d.ts +4 -0
- package/dist/Tabs/useRevealCurrentTab.d.ts.map +1 -0
- package/dist/Tabs/useRevealCurrentTab.js +85 -0
- package/dist/Tabs/useRevealCurrentTab.js.map +1 -0
- 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
- package/src/Tabs/PanelTabs.tsx +9 -2
- package/src/Tabs/RoutedTabs.tsx +6 -0
- package/src/Tabs/Tabs.mdx +8 -0
- package/src/Tabs/Tabs.stories.tsx +73 -0
- package/src/Tabs/Tabs.test.tsx +85 -0
- package/src/Tabs/useRevealCurrentTab.ts +93 -0
|
@@ -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":"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"}
|
|
@@ -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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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PanelTabs.d.ts","sourceRoot":"","sources":["../../src/Tabs/PanelTabs.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"PanelTabs.d.ts","sourceRoot":"","sources":["../../src/Tabs/PanelTabs.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAiBhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,eAAO,MAAM,SAAS,GAAI,2EAQvB,cAAc,KAAG,SAoLnB,CAAA"}
|
package/dist/Tabs/PanelTabs.js
CHANGED
|
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef } from "react";
|
|
|
4
4
|
import { toClassName } from "../toClassName.js";
|
|
5
5
|
import { TabTrigger } from "./TabTrigger.js";
|
|
6
6
|
import { toTabListClass } from "./tabStyles.js";
|
|
7
|
+
import { useRevealCurrentTab } from "./useRevealCurrentTab.js";
|
|
7
8
|
const ORIENTATION_KEYS = {
|
|
8
9
|
horizontal: {
|
|
9
10
|
next: "ArrowRight",
|
|
@@ -95,6 +96,7 @@ export const PanelTabs = ({ activation = "automatic", activeKey, className, labe
|
|
|
95
96
|
* `selectTabIndex` itself and got fixed there.
|
|
96
97
|
*/
|
|
97
98
|
const shownKey = selection.selectedValue ?? selection.pendingValue;
|
|
99
|
+
const revealRef = useRevealCurrentTab(shownKey, orientation);
|
|
98
100
|
const selectTab = useCallback((key) => {
|
|
99
101
|
// Both, because a pointer press moves focus *and* chooses.
|
|
100
102
|
// Only selecting would leave `automatic`'s effect below to
|
|
@@ -146,7 +148,10 @@ export const PanelTabs = ({ activation = "automatic", activeKey, className, labe
|
|
|
146
148
|
keyEvent.preventDefault();
|
|
147
149
|
select(activeValue);
|
|
148
150
|
}
|
|
149
|
-
}, ref:
|
|
151
|
+
}, ref: (element) => {
|
|
152
|
+
tablistRef.current = element;
|
|
153
|
+
revealRef.current = element;
|
|
154
|
+
}, role: "tablist", children: tabs.map((tab, index) => (_jsx(TabTrigger, { id: `${baseId}-tab-${index}`, isSelected: shownKey === tab.key, onSelect: selectTab, panelId: `${baseId}-panel-${index}`, registerFocus: focus.register, orientation: orientation, registerSelection: selection.register, tab: tab,
|
|
150
155
|
// The roving-tabindex rule read from the core rather
|
|
151
156
|
// than restated here: exactly one member is in the tab
|
|
152
157
|
// order, so Tab enters and leaves the bar once while
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PanelTabs.js","sourceRoot":"","sources":["../../src/Tabs/PanelTabs.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,cAAc,EACd,cAAc,EACd,eAAe,EACf,WAAW,GACZ,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"PanelTabs.js","sourceRoot":"","sources":["../../src/Tabs/PanelTabs.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RoutedTabs.d.ts","sourceRoot":"","sources":["../../src/Tabs/RoutedTabs.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAQtC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"RoutedTabs.d.ts","sourceRoot":"","sources":["../../src/Tabs/RoutedTabs.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAQtC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAI/C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,UAAU,GAAI,sDAMxB,aAAa,KAAG,SA8BlB,CAAA"}
|
package/dist/Tabs/RoutedTabs.js
CHANGED
|
@@ -3,6 +3,7 @@ import { getNavItemKey, resolveActiveKey, } from "../Nav/navItems.js";
|
|
|
3
3
|
import { toClassName } from "../toClassName.js";
|
|
4
4
|
import { TabLink } from "./TabLink.js";
|
|
5
5
|
import { toTabListClass } from "./tabStyles.js";
|
|
6
|
+
import { useRevealCurrentTab } from "./useRevealCurrentTab.js";
|
|
6
7
|
/**
|
|
7
8
|
* The routed bar: a named `<nav>` of real links wearing the tab
|
|
8
9
|
* paint.
|
|
@@ -20,7 +21,8 @@ import { toTabListClass } from "./tabStyles.js";
|
|
|
20
21
|
*/
|
|
21
22
|
export const RoutedTabs = ({ activeHref, className, label, orientation = "horizontal", tabs, }) => {
|
|
22
23
|
const activeKey = resolveActiveKey(tabs, activeHref);
|
|
23
|
-
|
|
24
|
+
const containerRef = useRevealCurrentTab(activeKey, orientation);
|
|
25
|
+
return (_jsx("nav", { ref: containerRef, "aria-label": label, className: toClassName(toTabListClass(orientation), className), children: tabs.map((tab) => {
|
|
24
26
|
const key = getNavItemKey(tab);
|
|
25
27
|
return (_jsx(TabLink, { isCurrent: key === activeKey, item: tab, orientation: orientation }, key));
|
|
26
28
|
}) }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RoutedTabs.js","sourceRoot":"","sources":["../../src/Tabs/RoutedTabs.tsx"],"names":[],"mappings":";AAEA,OAAO,EACL,aAAa,EACb,gBAAgB,GACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAe,CAAA;AAEvC,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"RoutedTabs.js","sourceRoot":"","sources":["../../src/Tabs/RoutedTabs.tsx"],"names":[],"mappings":";AAEA,OAAO,EACL,aAAa,EACb,gBAAgB,GACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAe,CAAA;AAEvC,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAE9D;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EACzB,UAAU,EACV,SAAS,EACT,KAAK,EACL,WAAW,GAAG,YAAY,EAC1B,IAAI,GACU,EAAa,EAAE;IAC7B,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;IACpD,MAAM,YAAY,GAAG,mBAAmB,CACtC,SAAS,EACT,WAAW,CACZ,CAAA;IAED,OAAO,CACL,cACE,GAAG,EAAE,YAAY,gBACL,KAAK,EACjB,SAAS,EAAE,WAAW,CACpB,cAAc,CAAC,WAAW,CAAC,EAC3B,SAAS,CACV,YAEA,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;YAChB,MAAM,GAAG,GAAG,aAAa,CAAC,GAAG,CAAC,CAAA;YAE9B,OAAO,CACL,KAAC,OAAO,IACN,SAAS,EAAE,GAAG,KAAK,SAAS,EAC5B,IAAI,EAAE,GAAG,EAET,WAAW,EAAE,WAAW,IADnB,GAAG,CAER,CACH,CAAA;QACH,CAAC,CAAC,GACE,CACP,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TabsOrientation } from "./tabItems.ts";
|
|
2
|
+
/** Reveal within the tab bar only; never scroll an ancestor or move focus. */
|
|
3
|
+
export declare function useRevealCurrentTab(currentKey: string | null, orientation: TabsOrientation): import("react").RefObject<HTMLElement | null>;
|
|
4
|
+
//# sourceMappingURL=useRevealCurrentTab.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useRevealCurrentTab.d.ts","sourceRoot":"","sources":["../../src/Tabs/useRevealCurrentTab.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAEpD,8EAA8E;AAC9E,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,WAAW,EAAE,eAAe,iDAsF7B"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef } from "react";
|
|
2
|
+
/** Reveal within the tab bar only; never scroll an ancestor or move focus. */
|
|
3
|
+
export function useRevealCurrentTab(currentKey, orientation) {
|
|
4
|
+
const containerRef = useRef(null);
|
|
5
|
+
useLayoutEffect(() => {
|
|
6
|
+
const container = containerRef.current;
|
|
7
|
+
if (!container || currentKey === null)
|
|
8
|
+
return;
|
|
9
|
+
let isActive = true;
|
|
10
|
+
const reveal = () => {
|
|
11
|
+
if (!isActive)
|
|
12
|
+
return;
|
|
13
|
+
// In manual panel mode, the focused tab can differ from the selected tab.
|
|
14
|
+
const focused = container.ownerDocument.activeElement;
|
|
15
|
+
const current = focused instanceof HTMLElement &&
|
|
16
|
+
container.contains(focused) &&
|
|
17
|
+
focused.matches('a, [role="tab"]')
|
|
18
|
+
? focused
|
|
19
|
+
: container.querySelector('[aria-current="page"], [aria-selected="true"]');
|
|
20
|
+
if (!current)
|
|
21
|
+
return;
|
|
22
|
+
const viewport = container.getBoundingClientRect();
|
|
23
|
+
const target = current.getBoundingClientRect();
|
|
24
|
+
const start = orientation === "horizontal"
|
|
25
|
+
? target.left
|
|
26
|
+
: target.top;
|
|
27
|
+
const end = orientation === "horizontal"
|
|
28
|
+
? target.right
|
|
29
|
+
: target.bottom;
|
|
30
|
+
const viewStart = orientation === "horizontal"
|
|
31
|
+
? viewport.left
|
|
32
|
+
: viewport.top;
|
|
33
|
+
const viewEnd = orientation === "horizontal"
|
|
34
|
+
? viewport.right
|
|
35
|
+
: viewport.bottom;
|
|
36
|
+
const hasOversize = end - start > viewEnd - viewStart;
|
|
37
|
+
const isRtl = orientation === "horizontal" &&
|
|
38
|
+
getComputedStyle(container).direction === "rtl";
|
|
39
|
+
const delta = hasOversize
|
|
40
|
+
? isRtl
|
|
41
|
+
? end - viewEnd
|
|
42
|
+
: start - viewStart
|
|
43
|
+
: start < viewStart
|
|
44
|
+
? start - viewStart
|
|
45
|
+
: end > viewEnd
|
|
46
|
+
? end - viewEnd
|
|
47
|
+
: 0;
|
|
48
|
+
if (orientation === "horizontal")
|
|
49
|
+
container.scrollLeft += delta;
|
|
50
|
+
else
|
|
51
|
+
container.scrollTop += delta;
|
|
52
|
+
};
|
|
53
|
+
const frame = { id: 0 };
|
|
54
|
+
const scheduleReveal = () => {
|
|
55
|
+
cancelAnimationFrame(frame.id);
|
|
56
|
+
frame.id = requestAnimationFrame(reveal);
|
|
57
|
+
};
|
|
58
|
+
const sizes = new ResizeObserver(scheduleReveal);
|
|
59
|
+
const observeItems = () => {
|
|
60
|
+
sizes.disconnect();
|
|
61
|
+
sizes.observe(container);
|
|
62
|
+
for (const child of container.children)
|
|
63
|
+
sizes.observe(child);
|
|
64
|
+
reveal();
|
|
65
|
+
};
|
|
66
|
+
const contents = new MutationObserver(observeItems);
|
|
67
|
+
contents.observe(container, {
|
|
68
|
+
childList: true,
|
|
69
|
+
subtree: true,
|
|
70
|
+
characterData: true,
|
|
71
|
+
});
|
|
72
|
+
container.addEventListener("focusin", reveal);
|
|
73
|
+
observeItems();
|
|
74
|
+
void container.ownerDocument.fonts.ready.then(reveal);
|
|
75
|
+
return () => {
|
|
76
|
+
isActive = false;
|
|
77
|
+
cancelAnimationFrame(frame.id);
|
|
78
|
+
sizes.disconnect();
|
|
79
|
+
contents.disconnect();
|
|
80
|
+
container.removeEventListener("focusin", reveal);
|
|
81
|
+
};
|
|
82
|
+
}, [currentKey, orientation]);
|
|
83
|
+
return containerRef;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=useRevealCurrentTab.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useRevealCurrentTab.js","sourceRoot":"","sources":["../../src/Tabs/useRevealCurrentTab.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
|
])
|
package/src/Tabs/PanelTabs.tsx
CHANGED
|
@@ -6,12 +6,12 @@ import {
|
|
|
6
6
|
} from "@charcuterie/logic"
|
|
7
7
|
import type { ReactNode } from "react"
|
|
8
8
|
import { useCallback, useEffect, useRef } from "react"
|
|
9
|
-
|
|
10
9
|
import { toClassName } from "../toClassName.ts"
|
|
11
10
|
import type { TabsPanelProps } from "./Tabs.tsx"
|
|
12
11
|
import { TabTrigger } from "./TabTrigger.tsx"
|
|
13
12
|
import type { TabsOrientation } from "./tabItems.ts"
|
|
14
13
|
import { toTabListClass } from "./tabStyles.ts"
|
|
14
|
+
import { useRevealCurrentTab } from "./useRevealCurrentTab.ts"
|
|
15
15
|
|
|
16
16
|
const ORIENTATION_KEYS: Record<
|
|
17
17
|
TabsOrientation,
|
|
@@ -128,6 +128,10 @@ export const PanelTabs = ({
|
|
|
128
128
|
*/
|
|
129
129
|
const shownKey =
|
|
130
130
|
selection.selectedValue ?? selection.pendingValue
|
|
131
|
+
const revealRef = useRevealCurrentTab(
|
|
132
|
+
shownKey,
|
|
133
|
+
orientation,
|
|
134
|
+
)
|
|
131
135
|
|
|
132
136
|
const selectTab = useCallback(
|
|
133
137
|
(key: string) => {
|
|
@@ -214,7 +218,10 @@ export const PanelTabs = ({
|
|
|
214
218
|
select(activeValue)
|
|
215
219
|
}
|
|
216
220
|
}}
|
|
217
|
-
ref={
|
|
221
|
+
ref={(element) => {
|
|
222
|
+
tablistRef.current = element
|
|
223
|
+
revealRef.current = element
|
|
224
|
+
}}
|
|
218
225
|
role="tablist"
|
|
219
226
|
>
|
|
220
227
|
{tabs.map((tab, index) => (
|
package/src/Tabs/RoutedTabs.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import { toClassName } from "../toClassName.ts"
|
|
|
8
8
|
import { TabLink } from "./TabLink.tsx"
|
|
9
9
|
import type { TabsLinkProps } from "./Tabs.tsx"
|
|
10
10
|
import { toTabListClass } from "./tabStyles.ts"
|
|
11
|
+
import { useRevealCurrentTab } from "./useRevealCurrentTab.ts"
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* The routed bar: a named `<nav>` of real links wearing the tab
|
|
@@ -32,9 +33,14 @@ export const RoutedTabs = ({
|
|
|
32
33
|
tabs,
|
|
33
34
|
}: TabsLinkProps): ReactNode => {
|
|
34
35
|
const activeKey = resolveActiveKey(tabs, activeHref)
|
|
36
|
+
const containerRef = useRevealCurrentTab(
|
|
37
|
+
activeKey,
|
|
38
|
+
orientation,
|
|
39
|
+
)
|
|
35
40
|
|
|
36
41
|
return (
|
|
37
42
|
<nav
|
|
43
|
+
ref={containerRef}
|
|
38
44
|
aria-label={label}
|
|
39
45
|
className={toClassName(
|
|
40
46
|
toTabListClass(orientation),
|
package/src/Tabs/Tabs.mdx
CHANGED
|
@@ -194,3 +194,11 @@ bar below and the underline stays put until Enter: focus has moved to a tab that
|
|
|
194
194
|
selected, which is a state a three-kind model cannot represent at all.
|
|
195
195
|
|
|
196
196
|
<Canvas of={TabsStories.Manual} />
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
## Current tabs in a narrow bar
|
|
200
|
+
|
|
201
|
+
An overflowing bar keeps its current tab in view on first render, route changes,
|
|
202
|
+
and reflow. It scrolls only itself, without moving focus or the surrounding page.
|
|
203
|
+
Manual panel activation keeps the focused tab visible even while another panel
|
|
204
|
+
remains selected. A reader can still scroll to other tabs.
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters"
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
3
|
|
|
4
|
+
import { useState } from "react"
|
|
4
5
|
import { toStoryChoice } from "../argTypes.storyHelpers.ts"
|
|
5
6
|
import { Badge } from "../Badge/Badge.tsx"
|
|
7
|
+
import { Button } from "../Button/Button.tsx"
|
|
6
8
|
import {
|
|
7
9
|
ContainerBoard,
|
|
8
10
|
StoryCell,
|
|
9
11
|
StoryGrid,
|
|
10
12
|
} from "../board.storyHelpers.tsx"
|
|
13
|
+
import { Field } from "../Field/Field.tsx"
|
|
11
14
|
import { ProgressBar } from "../ProgressBar/ProgressBar.tsx"
|
|
12
15
|
import { Tabs } from "./Tabs.tsx"
|
|
13
16
|
import type { TabItem, TabLinkItem } from "./tabItems.ts"
|
|
@@ -319,3 +322,73 @@ export const RoutedAllStates: Story = {
|
|
|
319
322
|
/>
|
|
320
323
|
),
|
|
321
324
|
}
|
|
325
|
+
|
|
326
|
+
const OVERFLOW_ROUTES: TabLinkItem[] = [
|
|
327
|
+
{ href: "/sections/activity", label: "Recent activity" },
|
|
328
|
+
{ href: "/sections/files", label: "Saved files" },
|
|
329
|
+
{ href: "/sections/builds", label: "Prepared builds" },
|
|
330
|
+
{ href: "/sections/reports", label: "Reports" },
|
|
331
|
+
]
|
|
332
|
+
export const CurrentTabVisible: Story = {
|
|
333
|
+
args: { label: "Sections", tabs: BAY_TABS },
|
|
334
|
+
render: () => {
|
|
335
|
+
const [href, setHref] = useState("/sections/reports")
|
|
336
|
+
const [inlineSize, setInlineSize] = useState(240)
|
|
337
|
+
return (
|
|
338
|
+
<div className="flex flex-col gap-4">
|
|
339
|
+
<Field label="Available width">
|
|
340
|
+
<input
|
|
341
|
+
type="number"
|
|
342
|
+
min={100}
|
|
343
|
+
max={500}
|
|
344
|
+
value={inlineSize}
|
|
345
|
+
onChange={(event) =>
|
|
346
|
+
setInlineSize(
|
|
347
|
+
Number(event.currentTarget.value),
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
/>
|
|
351
|
+
</Field>
|
|
352
|
+
<div className="flex flex-wrap gap-2">
|
|
353
|
+
<Button
|
|
354
|
+
onClick={() => setHref("/sections/activity")}
|
|
355
|
+
>
|
|
356
|
+
Show activity
|
|
357
|
+
</Button>
|
|
358
|
+
<Button
|
|
359
|
+
onClick={() => setHref("/sections/reports")}
|
|
360
|
+
>
|
|
361
|
+
Show reports
|
|
362
|
+
</Button>
|
|
363
|
+
</div>
|
|
364
|
+
<div style={{ inlineSize, maxInlineSize: "100%" }}>
|
|
365
|
+
<Tabs
|
|
366
|
+
label="Current routed section"
|
|
367
|
+
activeHref={href}
|
|
368
|
+
tabs={OVERFLOW_ROUTES}
|
|
369
|
+
/>
|
|
370
|
+
</div>
|
|
371
|
+
<div
|
|
372
|
+
dir="rtl"
|
|
373
|
+
style={{ inlineSize: 240, maxInlineSize: "100%" }}
|
|
374
|
+
>
|
|
375
|
+
<Tabs
|
|
376
|
+
label="Current RTL section"
|
|
377
|
+
activeHref="/sections/reports"
|
|
378
|
+
tabs={OVERFLOW_ROUTES}
|
|
379
|
+
/>
|
|
380
|
+
</div>
|
|
381
|
+
<div
|
|
382
|
+
style={{ inlineSize: 150, maxInlineSize: "100%" }}
|
|
383
|
+
>
|
|
384
|
+
<Tabs
|
|
385
|
+
label="Current manual panel"
|
|
386
|
+
activation="manual"
|
|
387
|
+
activeKey="disc"
|
|
388
|
+
tabs={BAY_TABS}
|
|
389
|
+
/>
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
)
|
|
393
|
+
},
|
|
394
|
+
}
|
package/src/Tabs/Tabs.test.tsx
CHANGED
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
Routed,
|
|
17
17
|
RoutedAllStates,
|
|
18
18
|
RoutedAllVariants,
|
|
19
|
+
CurrentTabVisible,
|
|
19
20
|
} = composeStories(stories)
|
|
20
21
|
|
|
21
22
|
test("selecting a tab wires the panel back to it", async () => {
|
|
@@ -450,3 +451,87 @@ test("a routed tab and a panel tab are painted identically", async () => {
|
|
|
450
451
|
)
|
|
451
452
|
}
|
|
452
453
|
})
|
|
454
|
+
|
|
455
|
+
test("current routed and panel tabs remain visible without moving focus", async () => {
|
|
456
|
+
const { canvas } = await mountStory(CurrentTabVisible)
|
|
457
|
+
const assertVisible = async (
|
|
458
|
+
bar: HTMLElement,
|
|
459
|
+
selector: string,
|
|
460
|
+
) => {
|
|
461
|
+
await waitFor(() => {
|
|
462
|
+
const selected =
|
|
463
|
+
bar.querySelector<HTMLElement>(selector)
|
|
464
|
+
expect(selected).not.toBeNull()
|
|
465
|
+
const item = selected?.getBoundingClientRect()
|
|
466
|
+
const bounds = bar.getBoundingClientRect()
|
|
467
|
+
expect(
|
|
468
|
+
item?.left ?? -Infinity,
|
|
469
|
+
).toBeGreaterThanOrEqual(bounds.left - 1)
|
|
470
|
+
expect(item?.right ?? Infinity).toBeLessThanOrEqual(
|
|
471
|
+
bounds.right + 1,
|
|
472
|
+
)
|
|
473
|
+
})
|
|
474
|
+
}
|
|
475
|
+
const routed = expectAgentDrivable(canvas, {
|
|
476
|
+
role: "navigation",
|
|
477
|
+
name: "Current routed section",
|
|
478
|
+
})
|
|
479
|
+
const rtl = expectAgentDrivable(canvas, {
|
|
480
|
+
role: "navigation",
|
|
481
|
+
name: "Current RTL section",
|
|
482
|
+
})
|
|
483
|
+
const panel = expectAgentDrivable(canvas, {
|
|
484
|
+
role: "tablist",
|
|
485
|
+
name: "Current manual panel",
|
|
486
|
+
})
|
|
487
|
+
await assertVisible(routed, '[aria-current="page"]')
|
|
488
|
+
await assertVisible(rtl, '[aria-current="page"]')
|
|
489
|
+
await assertVisible(panel, '[aria-selected="true"]')
|
|
490
|
+
const width = expectAgentDrivable(canvas, {
|
|
491
|
+
role: "spinbutton",
|
|
492
|
+
name: "Available width",
|
|
493
|
+
})
|
|
494
|
+
await userEvent.clear(width)
|
|
495
|
+
await userEvent.type(width, "160")
|
|
496
|
+
await assertVisible(routed, '[aria-current="page"]')
|
|
497
|
+
await expect(width).toHaveFocus()
|
|
498
|
+
const activity = expectAgentDrivable(canvas, {
|
|
499
|
+
role: "button",
|
|
500
|
+
name: "Show activity",
|
|
501
|
+
})
|
|
502
|
+
await userEvent.click(activity)
|
|
503
|
+
await assertVisible(routed, '[aria-current="page"]')
|
|
504
|
+
await expect(activity).toHaveFocus()
|
|
505
|
+
const reports = expectAgentDrivable(canvas, {
|
|
506
|
+
role: "button",
|
|
507
|
+
name: "Show reports",
|
|
508
|
+
})
|
|
509
|
+
await userEvent.click(reports)
|
|
510
|
+
await assertVisible(routed, '[aria-current="page"]')
|
|
511
|
+
await expect(reports).toHaveFocus()
|
|
512
|
+
await userEvent.clear(width)
|
|
513
|
+
await userEvent.type(width, "80")
|
|
514
|
+
await userEvent.click(activity)
|
|
515
|
+
await waitFor(() => {
|
|
516
|
+
const bounds = routed.getBoundingClientRect()
|
|
517
|
+
const selected = routed
|
|
518
|
+
.querySelector<HTMLElement>('[aria-current="page"]')
|
|
519
|
+
?.getBoundingClientRect()
|
|
520
|
+
expect(
|
|
521
|
+
selected?.left ?? -Infinity,
|
|
522
|
+
).toBeGreaterThanOrEqual(bounds.left - 1)
|
|
523
|
+
expect(Math.abs(routed.scrollLeft)).toBeLessThanOrEqual(
|
|
524
|
+
1,
|
|
525
|
+
)
|
|
526
|
+
})
|
|
527
|
+
const disc = panel.querySelector<HTMLElement>(
|
|
528
|
+
'[aria-selected="true"]',
|
|
529
|
+
)
|
|
530
|
+
disc?.focus()
|
|
531
|
+
await userEvent.keyboard("{ArrowLeft}")
|
|
532
|
+
await assertVisible(panel, ":focus")
|
|
533
|
+
await expect(disc).toHaveAttribute(
|
|
534
|
+
"aria-selected",
|
|
535
|
+
"true",
|
|
536
|
+
)
|
|
537
|
+
})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef } from "react"
|
|
2
|
+
import type { TabsOrientation } from "./tabItems.ts"
|
|
3
|
+
|
|
4
|
+
/** Reveal within the tab bar only; never scroll an ancestor or move focus. */
|
|
5
|
+
export function useRevealCurrentTab(
|
|
6
|
+
currentKey: string | null,
|
|
7
|
+
orientation: TabsOrientation,
|
|
8
|
+
) {
|
|
9
|
+
const containerRef = useRef<HTMLElement>(null)
|
|
10
|
+
useLayoutEffect(() => {
|
|
11
|
+
const container = containerRef.current
|
|
12
|
+
if (!container || currentKey === null) return
|
|
13
|
+
let isActive = true
|
|
14
|
+
const reveal = () => {
|
|
15
|
+
if (!isActive) return
|
|
16
|
+
// In manual panel mode, the focused tab can differ from the selected tab.
|
|
17
|
+
const focused = container.ownerDocument.activeElement
|
|
18
|
+
const current =
|
|
19
|
+
focused instanceof HTMLElement &&
|
|
20
|
+
container.contains(focused) &&
|
|
21
|
+
focused.matches('a, [role="tab"]')
|
|
22
|
+
? focused
|
|
23
|
+
: container.querySelector<HTMLElement>(
|
|
24
|
+
'[aria-current="page"], [aria-selected="true"]',
|
|
25
|
+
)
|
|
26
|
+
if (!current) return
|
|
27
|
+
const viewport = container.getBoundingClientRect()
|
|
28
|
+
const target = current.getBoundingClientRect()
|
|
29
|
+
const start =
|
|
30
|
+
orientation === "horizontal"
|
|
31
|
+
? target.left
|
|
32
|
+
: target.top
|
|
33
|
+
const end =
|
|
34
|
+
orientation === "horizontal"
|
|
35
|
+
? target.right
|
|
36
|
+
: target.bottom
|
|
37
|
+
const viewStart =
|
|
38
|
+
orientation === "horizontal"
|
|
39
|
+
? viewport.left
|
|
40
|
+
: viewport.top
|
|
41
|
+
const viewEnd =
|
|
42
|
+
orientation === "horizontal"
|
|
43
|
+
? viewport.right
|
|
44
|
+
: viewport.bottom
|
|
45
|
+
const hasOversize = end - start > viewEnd - viewStart
|
|
46
|
+
const isRtl =
|
|
47
|
+
orientation === "horizontal" &&
|
|
48
|
+
getComputedStyle(container).direction === "rtl"
|
|
49
|
+
const delta = hasOversize
|
|
50
|
+
? isRtl
|
|
51
|
+
? end - viewEnd
|
|
52
|
+
: start - viewStart
|
|
53
|
+
: start < viewStart
|
|
54
|
+
? start - viewStart
|
|
55
|
+
: end > viewEnd
|
|
56
|
+
? end - viewEnd
|
|
57
|
+
: 0
|
|
58
|
+
if (orientation === "horizontal")
|
|
59
|
+
container.scrollLeft += delta
|
|
60
|
+
else container.scrollTop += delta
|
|
61
|
+
}
|
|
62
|
+
const frame = { id: 0 }
|
|
63
|
+
const scheduleReveal = () => {
|
|
64
|
+
cancelAnimationFrame(frame.id)
|
|
65
|
+
frame.id = requestAnimationFrame(reveal)
|
|
66
|
+
}
|
|
67
|
+
const sizes = new ResizeObserver(scheduleReveal)
|
|
68
|
+
const observeItems = () => {
|
|
69
|
+
sizes.disconnect()
|
|
70
|
+
sizes.observe(container)
|
|
71
|
+
for (const child of container.children)
|
|
72
|
+
sizes.observe(child)
|
|
73
|
+
reveal()
|
|
74
|
+
}
|
|
75
|
+
const contents = new MutationObserver(observeItems)
|
|
76
|
+
contents.observe(container, {
|
|
77
|
+
childList: true,
|
|
78
|
+
subtree: true,
|
|
79
|
+
characterData: true,
|
|
80
|
+
})
|
|
81
|
+
container.addEventListener("focusin", reveal)
|
|
82
|
+
observeItems()
|
|
83
|
+
void container.ownerDocument.fonts.ready.then(reveal)
|
|
84
|
+
return () => {
|
|
85
|
+
isActive = false
|
|
86
|
+
cancelAnimationFrame(frame.id)
|
|
87
|
+
sizes.disconnect()
|
|
88
|
+
contents.disconnect()
|
|
89
|
+
container.removeEventListener("focusin", reveal)
|
|
90
|
+
}
|
|
91
|
+
}, [currentKey, orientation])
|
|
92
|
+
return containerRef
|
|
93
|
+
}
|