@charcuterie/ui 1.1.0 → 2.0.0
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/Combobox/Combobox.d.ts +54 -0
- package/dist/Combobox/Combobox.d.ts.map +1 -0
- package/dist/Combobox/Combobox.js +216 -0
- package/dist/Combobox/Combobox.js.map +1 -0
- package/dist/Combobox/ComboboxOption.d.ts +36 -0
- package/dist/Combobox/ComboboxOption.d.ts.map +1 -0
- package/dist/Combobox/ComboboxOption.js +32 -0
- package/dist/Combobox/ComboboxOption.js.map +1 -0
- package/dist/Dialog/Dialog.d.ts +47 -0
- package/dist/Dialog/Dialog.d.ts.map +1 -0
- package/dist/Dialog/Dialog.js +52 -0
- package/dist/Dialog/Dialog.js.map +1 -0
- package/dist/Lightbox/Lightbox.d.ts +7 -7
- package/dist/Lightbox/Lightbox.js +9 -9
- package/dist/Lightbox/Lightbox.js.map +1 -1
- package/dist/Listbox/Listbox.d.ts +56 -0
- package/dist/Listbox/Listbox.d.ts.map +1 -0
- package/dist/Listbox/Listbox.js +151 -0
- package/dist/Listbox/Listbox.js.map +1 -0
- package/dist/Listbox/ListboxOption.d.ts +36 -0
- package/dist/Listbox/ListboxOption.d.ts.map +1 -0
- package/dist/Listbox/ListboxOption.js +42 -0
- package/dist/Listbox/ListboxOption.js.map +1 -0
- package/dist/Menu/Menu.d.ts.map +1 -1
- package/dist/Menu/Menu.js +47 -97
- package/dist/Menu/Menu.js.map +1 -1
- package/dist/Modal/Modal.d.ts +11 -59
- package/dist/Modal/Modal.d.ts.map +1 -1
- package/dist/Modal/Modal.js +28 -101
- package/dist/Modal/Modal.js.map +1 -1
- package/dist/Overlay/OverlayPanel.d.ts +50 -0
- package/dist/Overlay/OverlayPanel.d.ts.map +1 -0
- package/dist/Overlay/OverlayPanel.js +72 -0
- package/dist/Overlay/OverlayPanel.js.map +1 -0
- package/dist/Overlay/OverlayStack.d.ts +47 -0
- package/dist/Overlay/OverlayStack.d.ts.map +1 -0
- package/dist/Overlay/OverlayStack.js +46 -0
- package/dist/Overlay/OverlayStack.js.map +1 -0
- package/dist/Overlay/SharedBackdrop.d.ts +24 -0
- package/dist/Overlay/SharedBackdrop.d.ts.map +1 -0
- package/dist/Overlay/SharedBackdrop.js +28 -0
- package/dist/Overlay/SharedBackdrop.js.map +1 -0
- package/dist/Overlay/lockScrollBehind.d.ts.map +1 -0
- package/dist/Overlay/lockScrollBehind.js.map +1 -0
- package/dist/Overlay/overlayPanelClass.d.ts +17 -0
- package/dist/Overlay/overlayPanelClass.d.ts.map +1 -0
- package/dist/Overlay/overlayPanelClass.js +17 -0
- package/dist/Overlay/overlayPanelClass.js.map +1 -0
- package/dist/Overlay/useAnchoredOverlay.d.ts +97 -0
- package/dist/Overlay/useAnchoredOverlay.d.ts.map +1 -0
- package/dist/Overlay/useAnchoredOverlay.js +81 -0
- package/dist/Overlay/useAnchoredOverlay.js.map +1 -0
- package/dist/Popover/Popover.d.ts +16 -24
- package/dist/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +32 -68
- package/dist/Popover/Popover.js.map +1 -1
- package/dist/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/Tooltip/Tooltip.js +25 -25
- package/dist/Tooltip/Tooltip.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/mountStory.testHelpers.d.ts +1 -0
- package/dist/mountStory.testHelpers.d.ts.map +1 -1
- package/dist/mountStory.testHelpers.js +21 -1
- package/dist/mountStory.testHelpers.js.map +1 -1
- package/package.json +3 -2
- package/src/Combobox/Combobox.mdx +71 -0
- package/src/Combobox/Combobox.stories.tsx +245 -0
- package/src/Combobox/Combobox.test.tsx +289 -0
- package/src/Combobox/Combobox.tsx +513 -0
- package/src/Combobox/ComboboxOption.tsx +88 -0
- package/src/Dialog/Dialog.mdx +87 -0
- package/src/Dialog/Dialog.stories.tsx +347 -0
- package/src/Dialog/Dialog.test.tsx +312 -0
- package/src/Dialog/Dialog.tsx +127 -0
- package/src/Field/Field.test.tsx +6 -4
- package/src/Lightbox/Lightbox.mdx +9 -8
- package/src/Lightbox/Lightbox.stories.tsx +5 -5
- package/src/Lightbox/Lightbox.test.tsx +25 -36
- package/src/Lightbox/Lightbox.tsx +10 -10
- package/src/Listbox/Listbox.mdx +68 -0
- package/src/Listbox/Listbox.stories.tsx +205 -0
- package/src/Listbox/Listbox.test.tsx +198 -0
- package/src/Listbox/Listbox.tsx +295 -0
- package/src/Listbox/ListboxOption.tsx +104 -0
- package/src/Menu/Menu.mdx +11 -13
- package/src/Menu/Menu.test.tsx +47 -38
- package/src/Menu/Menu.tsx +81 -142
- package/src/Modal/Modal.mdx +29 -82
- package/src/Modal/Modal.stories.tsx +55 -188
- package/src/Modal/Modal.test.tsx +44 -177
- package/src/Modal/Modal.tsx +54 -204
- package/src/Overlay/OverlayPanel.tsx +195 -0
- package/src/Overlay/OverlayStack.test.tsx +235 -0
- package/src/Overlay/OverlayStack.tsx +132 -0
- package/src/Overlay/SharedBackdrop.tsx +40 -0
- package/src/Overlay/overlayPanelClass.ts +17 -0
- package/src/Overlay/useAnchoredOverlay.ts +197 -0
- package/src/Popover/Popover.mdx +17 -20
- package/src/Popover/Popover.stories.tsx +33 -0
- package/src/Popover/Popover.test.tsx +68 -32
- package/src/Popover/Popover.tsx +56 -95
- package/src/Tooltip/Tooltip.mdx +12 -9
- package/src/Tooltip/Tooltip.test.tsx +17 -19
- package/src/Tooltip/Tooltip.tsx +35 -32
- package/src/index.ts +13 -4
- package/src/mountStory.testHelpers.ts +25 -1
- package/src/sourceRules.test.ts +12 -3
- package/src/storyControls.test.ts +25 -12
- package/dist/Modal/lockScrollBehind.d.ts.map +0 -1
- package/dist/Modal/lockScrollBehind.js.map +0 -1
- /package/dist/{Modal → Overlay}/lockScrollBehind.d.ts +0 -0
- /package/dist/{Modal → Overlay}/lockScrollBehind.js +0 -0
- /package/src/{Modal → Overlay}/lockScrollBehind.ts +0 -0
package/dist/Modal/Modal.js
CHANGED
|
@@ -1,111 +1,38 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
2
|
-
import {
|
|
3
|
-
import { useEffect, useRef } from "react";
|
|
4
|
-
import { Button } from "../Button/Button.js";
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { OverlayPanel } from "../Overlay/OverlayPanel.js";
|
|
5
3
|
import { toClassName } from "../toClassName.js";
|
|
6
|
-
import { lockScrollBehind } from "./lockScrollBehind.js";
|
|
7
4
|
/**
|
|
8
|
-
*
|
|
9
|
-
* address bar, and a dialog whose footer is under it is a dialog
|
|
10
|
-
* with no buttons.
|
|
11
|
-
*/
|
|
12
|
-
const SIZE_CLASS = {
|
|
13
|
-
sm: "max-h-[85dvh] w-full max-w-sm",
|
|
14
|
-
md: "max-h-[85dvh] w-full max-w-md",
|
|
15
|
-
lg: "max-h-[85dvh] w-full max-w-2xl",
|
|
16
|
-
// M5 added this, because the scale jumped from 42rem straight to
|
|
17
|
-
// the whole viewport and rip-deck's capture tail lands in the
|
|
18
|
-
// gap: a MakeMKV robot log is fixed-width `MSG:5072,0,1,"…"`
|
|
19
|
-
// lines that wrap badly at 42rem, and a full-screen dialog over a
|
|
20
|
-
// nine-bay dashboard hides the thing being diagnosed. 56rem is
|
|
21
|
-
// what that app's hand-rolled `<dialog>` used before the swap.
|
|
22
|
-
xl: "max-h-[85dvh] w-full max-w-4xl",
|
|
23
|
-
full: "h-[100dvh] max-h-[100dvh] w-full max-w-full rounded-none",
|
|
24
|
-
};
|
|
25
|
-
/**
|
|
26
|
-
* Four repos, and **plex-channels and ripdeck independently
|
|
27
|
-
* converged on native `<dialog>`** — which is the design here
|
|
28
|
-
* rather than a shortcut. `showModal()` gives a focus trap,
|
|
29
|
-
* Escape, `::backdrop`, `inert` on everything behind it, and the
|
|
30
|
-
* **top layer**, from the platform, correctly, in every browser
|
|
31
|
-
* the fleet runs.
|
|
32
|
-
*
|
|
33
|
-
* What is not free — and the reason a component exists at all —
|
|
34
|
-
* is that a native `<dialog>` is a *self-owning* control: call
|
|
35
|
-
* `showModal()` and it decides it is open. That is the same
|
|
36
|
-
* conflict Radix has, and it gets the same answer as everywhere
|
|
37
|
-
* else in this library: **`isVisible` is the truth, and an effect
|
|
38
|
-
* makes the element agree with it.** Nothing here reads
|
|
39
|
-
* `dialogElement.open` as state; it is read only to avoid calling
|
|
40
|
-
* a method that would throw.
|
|
5
|
+
* The base layer. Backdrop, dismiss, and portal — and no chrome.
|
|
41
6
|
*
|
|
42
|
-
*
|
|
7
|
+
* This is the generic modal the library never had while `Modal` was
|
|
8
|
+
* secretly a *dialog*. A dialog is one kind of modal; so are a
|
|
9
|
+
* confirmation, a media lightbox, and anything else that wants a
|
|
10
|
+
* scrim, an Escape, and a click-outside. `Modal` is that foundation,
|
|
11
|
+
* and `Dialog` is `Modal` plus the heading/Close/footer/size chrome
|
|
12
|
+
* the old component shipped.
|
|
43
13
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* `
|
|
50
|
-
*
|
|
51
|
-
* and lose its scoping.
|
|
14
|
+
* It renders an `OverlayPanel`: portalled to `document.body` (escaping
|
|
15
|
+
* `overflow: hidden` the top layer could not), focus-trapped, stacked
|
|
16
|
+
* by portal append order, and dismissible only when it is the top of
|
|
17
|
+
* the stack. All `Modal` adds is a sensible surface — a bordered,
|
|
18
|
+
* rounded, elevated box clamped to `85dvh` — that a caller composes
|
|
19
|
+
* over with `className`. `Dialog` replaces the width and layout; a
|
|
20
|
+
* bare `Modal` sizes to its content.
|
|
52
21
|
*
|
|
53
|
-
* ###
|
|
22
|
+
* ### One accessible name, required
|
|
54
23
|
*
|
|
55
|
-
* A
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* `[open]` attribute the browser manages.
|
|
24
|
+
* A `role="dialog"` takes no name from its content, so pass exactly
|
|
25
|
+
* one of `aria-label` / `aria-labelledby`. Missing both is a dev-time
|
|
26
|
+
* warning from `OverlayPanel`, not a throw — the lesson `Spinner`,
|
|
27
|
+
* `Popover`, and the old `Modal` each learned.
|
|
60
28
|
*
|
|
61
|
-
* ###
|
|
29
|
+
* ### Stacking is zero-config, until it is not
|
|
62
30
|
*
|
|
63
|
-
* `
|
|
64
|
-
*
|
|
65
|
-
*
|
|
31
|
+
* A lone `Modal` renders its own scrim with no setup. Only when modals
|
|
32
|
+
* **stack** — one opened from inside another — does the app need an
|
|
33
|
+
* `OverlayStackProvider` at its root, so the N of them share one scrim
|
|
34
|
+
* and dismiss top-first.
|
|
66
35
|
*/
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const headingId = useUniqueId();
|
|
70
|
-
const Heading = `h${headingLevel}`;
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
const dialogElement = dialogRef.current;
|
|
73
|
-
if (!dialogElement) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
// Guarded both ways. `showModal()` on an already-open dialog
|
|
77
|
-
// throws `InvalidStateError`, and `close()` on a closed one
|
|
78
|
-
// fires a spurious `close` event — either turns a re-render
|
|
79
|
-
// into a bug.
|
|
80
|
-
if (isVisible && !dialogElement.open) {
|
|
81
|
-
dialogElement.showModal();
|
|
82
|
-
}
|
|
83
|
-
if (!isVisible && dialogElement.open) {
|
|
84
|
-
dialogElement.close();
|
|
85
|
-
}
|
|
86
|
-
}, [isVisible]);
|
|
87
|
-
useEffect(() => (isVisible ? lockScrollBehind() : undefined), [isVisible]);
|
|
88
|
-
return (_jsxs("dialog", { ...dialogProps, "aria-labelledby": headingId, className: toClassName(
|
|
89
|
-
// `m-auto` and `p-0` undo the UA stylesheet, which centres
|
|
90
|
-
// by margin and adds padding of its own.
|
|
91
|
-
"m-auto flex-col overflow-hidden rounded-lg border border-border-default bg-surface-overlay p-0 text-content-primary shadow-high backdrop:bg-scrim open:flex", SIZE_CLASS[size], className), onCancel: (cancelEvent) => {
|
|
92
|
-
// Escape. Cancelled rather than ignored when the dialog is
|
|
93
|
-
// not dismissable: letting the browser close it would
|
|
94
|
-
// leave `isVisible` true and the element shut, which is
|
|
95
|
-
// precisely the desync this component exists to prevent.
|
|
96
|
-
cancelEvent.preventDefault();
|
|
97
|
-
if (isDismissable) {
|
|
98
|
-
onClose();
|
|
99
|
-
}
|
|
100
|
-
}, onClick: (clickEvent) => {
|
|
101
|
-
// The backdrop is not an element — a press on it targets
|
|
102
|
-
// the `<dialog>` itself, while a press on the content
|
|
103
|
-
// targets something inside. Comparing target to
|
|
104
|
-
// currentTarget is the whole test.
|
|
105
|
-
if (isDismissable &&
|
|
106
|
-
clickEvent.target === clickEvent.currentTarget) {
|
|
107
|
-
onClose();
|
|
108
|
-
}
|
|
109
|
-
}, ref: dialogRef, children: [_jsxs("header", { className: "flex shrink-0 items-start justify-between gap-3 border-b border-border-subtle p-4", children: [_jsx(Heading, { className: "font-semibold text-lg leading-tight", id: headingId, children: heading }), isDismissable ? (_jsx(Button, { appearance: "ghost", intent: "neutral", onClick: onClose, size: "sm", children: "Close" })) : null] }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto p-4", children: children }), footer ? (_jsx("footer", { className: "flex shrink-0 flex-wrap justify-end gap-2 border-t border-border-subtle p-4", children: footer })) : null] }));
|
|
110
|
-
};
|
|
36
|
+
const MODAL_BOX_CLASS = "flex max-h-[85dvh] flex-col overflow-hidden rounded-lg border border-border-default bg-surface-overlay text-content-primary shadow-high";
|
|
37
|
+
export const Modal = ({ "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, children, className, isDismissable = true, isVisible, onClose, role = "dialog", }) => (_jsx(OverlayPanel, { "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, className: toClassName(MODAL_BOX_CLASS, className), isDismissable: isDismissable, isVisible: isVisible, onClose: onClose, role: role, children: children }));
|
|
111
38
|
//# sourceMappingURL=Modal.js.map
|
package/dist/Modal/Modal.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Modal.js","sourceRoot":"","sources":["../../src/Modal/Modal.tsx"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"Modal.js","sourceRoot":"","sources":["../../src/Modal/Modal.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA6B,CAAA;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAkB/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,eAAe,GACnB,yIAAyI,CAAA;AAE3I,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,EACpB,YAAY,EAAE,SAAS,EACvB,iBAAiB,EAAE,cAAc,EACjC,QAAQ,EACR,SAAS,EACT,aAAa,GAAG,IAAI,EACpB,SAAS,EACT,OAAO,EACP,IAAI,GAAG,QAAQ,GACJ,EAAa,EAAE,CAAC,CAC3B,KAAC,YAAY,kBACC,SAAS,qBACJ,cAAc,EAC/B,SAAS,EAAE,WAAW,CAAC,eAAe,EAAE,SAAS,CAAC,EAClD,aAAa,EAAE,aAAa,EAC5B,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,IAAI,YAET,QAAQ,GACI,CAChB,CAAA"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The portalled, backdrop-backed, focus-trapping panel that `Modal`
|
|
4
|
+
* renders. No chrome — that is `Dialog`'s job — only the four things
|
|
5
|
+
* every backdrop overlay needs: it portals to `document.body`, a
|
|
6
|
+
* scrim stands behind it, focus is trapped inside it, and Escape or an
|
|
7
|
+
* outside press closes it.
|
|
8
|
+
*
|
|
9
|
+
* ### Portalled, and the top layer is gone on purpose
|
|
10
|
+
*
|
|
11
|
+
* This supersedes the `<dialog>.showModal()` top-layer approach. The
|
|
12
|
+
* top layer paints above everything but cannot escape an
|
|
13
|
+
* `overflow: hidden` ancestor's *clipping* in every case the fleet
|
|
14
|
+
* hit; a portal to `document.body` does. The objection the old
|
|
15
|
+
* decision raised — a portal moves the node out of a scoped query —
|
|
16
|
+
* is answered by `useRole` writing `aria-controls`/`aria-labelledby`
|
|
17
|
+
* so the trigger→panel link is followable across the boundary, and by
|
|
18
|
+
* the stories scoping panels to `within(document.body)` while the
|
|
19
|
+
* structural `expectAgentDrivable` needs no change.
|
|
20
|
+
*
|
|
21
|
+
* ### Only the top of the stack is live
|
|
22
|
+
*
|
|
23
|
+
* `useDismiss` (Escape + outside press) is enabled **only when this is
|
|
24
|
+
* the top** of the stack, so an outside press or Escape closes the
|
|
25
|
+
* innermost modal and nothing under it — the same rule the platform's
|
|
26
|
+
* nested `<dialog>` gave for free, without a hand-rolled document
|
|
27
|
+
* keydown racing the focus manager. Everything below the top is
|
|
28
|
+
* `inert`, which is what `expectHiddenFromAgents` asserts and what
|
|
29
|
+
* makes `FloatingFocusManager`'s trap unambiguous.
|
|
30
|
+
*
|
|
31
|
+
* ### One accessible name, required
|
|
32
|
+
*
|
|
33
|
+
* A `role="dialog"` takes no name from its content, so one of
|
|
34
|
+
* `aria-label` / `aria-labelledby` must be present. Missing both is a
|
|
35
|
+
* dev-time warning, not a throw — the lesson `Spinner`, `Popover`, and
|
|
36
|
+
* `Modal` each learned, applied without breaking a render.
|
|
37
|
+
*/
|
|
38
|
+
export type OverlayPanelProps = {
|
|
39
|
+
"aria-label"?: string;
|
|
40
|
+
"aria-labelledby"?: string;
|
|
41
|
+
children: ReactNode;
|
|
42
|
+
className?: string;
|
|
43
|
+
/** Escape and an outside press both close. */
|
|
44
|
+
isDismissable?: boolean;
|
|
45
|
+
isVisible: boolean;
|
|
46
|
+
onClose: () => void;
|
|
47
|
+
role?: "alertdialog" | "dialog";
|
|
48
|
+
};
|
|
49
|
+
export declare const OverlayPanel: ({ "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, children, className, isDismissable, isVisible, onClose, role, }: OverlayPanelProps) => ReactNode;
|
|
50
|
+
//# sourceMappingURL=OverlayPanel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OverlayPanel.d.ts","sourceRoot":"","sources":["../../src/Overlay/OverlayPanel.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAOtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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,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,+HAS1B,iBAAiB,KAAG,SAsHtB,CAAA"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useLatestRef, useUniqueId, } from "@charcuterie/logic";
|
|
3
|
+
import { FloatingFocusManager, FloatingPortal, useDismiss, useFloating, useInteractions, useRole, } from "@floating-ui/react";
|
|
4
|
+
import { useCallback, useEffect } from "react";
|
|
5
|
+
import { lockScrollBehind } from "./lockScrollBehind.js";
|
|
6
|
+
import { useOverlayStack } from "./OverlayStack.js";
|
|
7
|
+
import { SharedBackdrop } from "./SharedBackdrop.js";
|
|
8
|
+
export const OverlayPanel = ({ "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, children, className, isDismissable = true, isVisible, onClose, role = "dialog", }) => {
|
|
9
|
+
const stack = useOverlayStack();
|
|
10
|
+
const id = useUniqueId();
|
|
11
|
+
// A stable close, so registering it does not churn the stack as the
|
|
12
|
+
// caller's `onClose` closure changes identity between renders.
|
|
13
|
+
const onCloseRef = useLatestRef(onClose);
|
|
14
|
+
const stableOnClose = useCallback(() => {
|
|
15
|
+
onCloseRef.current();
|
|
16
|
+
}, [onCloseRef]);
|
|
17
|
+
const isTop = !stack.isProvided || stack.topId === id;
|
|
18
|
+
const { context, refs } = useFloating({
|
|
19
|
+
open: isVisible,
|
|
20
|
+
onOpenChange: (isNextVisible) => {
|
|
21
|
+
if (!isNextVisible) {
|
|
22
|
+
onClose();
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
const { getFloatingProps } = useInteractions([
|
|
27
|
+
useDismiss(context, {
|
|
28
|
+
escapeKey: isDismissable && isTop,
|
|
29
|
+
// The top's outside press is the backdrop click. A per-panel
|
|
30
|
+
// `outsidePress` on a *lower* modal would close it too, so it is
|
|
31
|
+
// scoped to the top — which is also why the scrim needs no click
|
|
32
|
+
// handler of its own.
|
|
33
|
+
outsidePress: isDismissable && isTop,
|
|
34
|
+
}),
|
|
35
|
+
useRole(context, { role }),
|
|
36
|
+
]);
|
|
37
|
+
const { register, unregister } = stack;
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!isVisible) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
register({ id, onClose: stableOnClose });
|
|
43
|
+
return () => {
|
|
44
|
+
unregister(id);
|
|
45
|
+
};
|
|
46
|
+
}, [id, isVisible, register, stableOnClose, unregister]);
|
|
47
|
+
// Ref-counted through `lockScrollBehind`'s module scope, so N
|
|
48
|
+
// stacked panels lock the page once and free it only when the last
|
|
49
|
+
// closes — with or without a provider.
|
|
50
|
+
useEffect(() => (isVisible ? lockScrollBehind() : undefined), [isVisible]);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (isVisible &&
|
|
53
|
+
ariaLabel === undefined &&
|
|
54
|
+
ariaLabelledBy === undefined) {
|
|
55
|
+
console.warn('OverlayPanel: a modal needs an accessible name. Pass `aria-label` or `aria-labelledby` — a `role="dialog"` takes none from its content and is announced as just "dialog".');
|
|
56
|
+
}
|
|
57
|
+
}, [ariaLabel, ariaLabelledBy, isVisible]);
|
|
58
|
+
if (!isVisible) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return (_jsxs(_Fragment, { children: [stack.isProvided ? null : (_jsx(SharedBackdrop, { isVisible: true })), _jsx(FloatingPortal, { children: _jsx("div", { className: "fixed inset-0 z-[var(--layer-modal)] flex items-center justify-center", children: _jsx(FloatingFocusManager, { context: context,
|
|
62
|
+
// Only the top traps focus; a lower panel is `inert`
|
|
63
|
+
// anyway, and two live traps fight over one caret.
|
|
64
|
+
disabled: !isTop, modal: true, children: _jsx("div", { ...getFloatingProps(), "aria-label": ariaLabel,
|
|
65
|
+
"aria-labelledby": ariaLabelledBy, className: className,
|
|
66
|
+
// The fallback the plan calls for: `inert` on every panel
|
|
67
|
+
// but the top, so a lower modal is unreachable across the
|
|
68
|
+
// portal boundary even where `FloatingFocusManager`'s
|
|
69
|
+
// outside-hiding does not reach a sibling portal.
|
|
70
|
+
inert: isTop ? undefined : true, ref: refs.setFloating, role: role, children: children }) }) }) })] }));
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=OverlayPanel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OverlayPanel.js","sourceRoot":"","sources":["../../src/Overlay/OverlayPanel.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The ordered stack of open **backdrop** modals, and the one shared
|
|
4
|
+
* scrim that stands behind all of them.
|
|
5
|
+
*
|
|
6
|
+
* ### Why a stack at all
|
|
7
|
+
*
|
|
8
|
+
* A modal must know two things it cannot see for itself: whether it is
|
|
9
|
+
* the **top** of the stack (only the top traps focus and answers
|
|
10
|
+
* Escape / an outside press; everything under it is `inert`), and how
|
|
11
|
+
* **deep** the stack is (the scrim shows while at least one modal is
|
|
12
|
+
* open and hides when the last closes). An `OverlayPanel` registers on
|
|
13
|
+
* open and unregisters on close, and reads `topId`/`depth` back.
|
|
14
|
+
*
|
|
15
|
+
* ### The provider is optional
|
|
16
|
+
*
|
|
17
|
+
* `useOverlayStack()` has a self-hosting default — `isProvided:
|
|
18
|
+
* false`, a no-op register, `depth: 0` — so a lone `Modal` needs no
|
|
19
|
+
* setup: it sees it is unprovided and renders its own scrim, treating
|
|
20
|
+
* itself as the top. Only *stacking* needs `OverlayStackProvider` at
|
|
21
|
+
* the app root, where a single shared scrim and real top-of-stack
|
|
22
|
+
* bookkeeping replace N self-hosted scrims fighting for the same
|
|
23
|
+
* corner.
|
|
24
|
+
*/
|
|
25
|
+
export type OverlayStackEntry = {
|
|
26
|
+
id: string;
|
|
27
|
+
onClose: () => void;
|
|
28
|
+
};
|
|
29
|
+
export type OverlayStackValue = {
|
|
30
|
+
depth: number;
|
|
31
|
+
/**
|
|
32
|
+
* `false` in the context default. An `OverlayPanel` reads this to
|
|
33
|
+
* decide whether to render its own scrim (unprovided) or defer to
|
|
34
|
+
* the provider's shared one.
|
|
35
|
+
*/
|
|
36
|
+
isProvided: boolean;
|
|
37
|
+
register: (entry: OverlayStackEntry) => void;
|
|
38
|
+
/** Closes the top entry — the imperative twin of an outside press. */
|
|
39
|
+
requestCloseTop: () => void;
|
|
40
|
+
topId: null | string;
|
|
41
|
+
unregister: (id: string) => void;
|
|
42
|
+
};
|
|
43
|
+
export declare const useOverlayStack: () => OverlayStackValue;
|
|
44
|
+
export declare const OverlayStackProvider: ({ children, }: {
|
|
45
|
+
children: ReactNode;
|
|
46
|
+
}) => ReactNode;
|
|
47
|
+
//# sourceMappingURL=OverlayStack.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OverlayStack.d.ts","sourceRoot":"","sources":["../../src/Overlay/OverlayStack.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAWtC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,UAAU,EAAE,OAAO,CAAA;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAC5C,sEAAsE;IACtE,eAAe,EAAE,MAAM,IAAI,CAAA;IAC3B,KAAK,EAAE,IAAI,GAAG,MAAM,CAAA;IACpB,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC,CAAA;AAcD,eAAO,MAAM,eAAe,QAAO,iBACF,CAAA;AAEjC,eAAO,MAAM,oBAAoB,GAAI,eAElC;IACD,QAAQ,EAAE,SAAS,CAAA;CACpB,KAAG,SAyDH,CAAA"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useLatestRef } from "@charcuterie/logic";
|
|
3
|
+
import { createContext, useCallback, useContext, useMemo, useState, } from "react";
|
|
4
|
+
import { SharedBackdrop } from "./SharedBackdrop.js";
|
|
5
|
+
const DEFAULT_STACK = {
|
|
6
|
+
depth: 0,
|
|
7
|
+
isProvided: false,
|
|
8
|
+
register: () => { },
|
|
9
|
+
requestCloseTop: () => { },
|
|
10
|
+
topId: null,
|
|
11
|
+
unregister: () => { },
|
|
12
|
+
};
|
|
13
|
+
const OverlayStackContext = createContext(DEFAULT_STACK);
|
|
14
|
+
export const useOverlayStack = () => useContext(OverlayStackContext);
|
|
15
|
+
export const OverlayStackProvider = ({ children, }) => {
|
|
16
|
+
const [entries, setEntries] = useState([]);
|
|
17
|
+
// Read through a ref so `requestCloseTop` stays stable while still
|
|
18
|
+
// seeing the live top.
|
|
19
|
+
const entriesRef = useLatestRef(entries);
|
|
20
|
+
const register = useCallback((entry) => {
|
|
21
|
+
setEntries((previous) =>
|
|
22
|
+
// In place when the id is already present — an `onClose`
|
|
23
|
+
// update must not reorder the stack, or a re-render would
|
|
24
|
+
// shuffle which modal is "top". New ids append, which is what
|
|
25
|
+
// makes the last-registered one the top.
|
|
26
|
+
previous.some((one) => one.id === entry.id)
|
|
27
|
+
? previous.map((one) => one.id === entry.id ? entry : one)
|
|
28
|
+
: [...previous, entry]);
|
|
29
|
+
}, []);
|
|
30
|
+
const unregister = useCallback((id) => {
|
|
31
|
+
setEntries((previous) => previous.filter((one) => one.id !== id));
|
|
32
|
+
}, []);
|
|
33
|
+
const requestCloseTop = useCallback(() => {
|
|
34
|
+
entriesRef.current.at(-1)?.onClose();
|
|
35
|
+
}, [entriesRef]);
|
|
36
|
+
const value = useMemo(() => ({
|
|
37
|
+
depth: entries.length,
|
|
38
|
+
isProvided: true,
|
|
39
|
+
register,
|
|
40
|
+
requestCloseTop,
|
|
41
|
+
topId: entries.at(-1)?.id ?? null,
|
|
42
|
+
unregister,
|
|
43
|
+
}), [entries, register, requestCloseTop, unregister]);
|
|
44
|
+
return (_jsxs(OverlayStackContext.Provider, { value: value, children: [_jsx(SharedBackdrop, { isVisible: entries.length > 0 }), children] }));
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=OverlayStack.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OverlayStack.js","sourceRoot":"","sources":["../../src/Overlay/OverlayStack.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEjD,OAAO,EACL,aAAa,EACb,WAAW,EACX,UAAU,EACV,OAAO,EACP,QAAQ,GACT,MAAM,OAAO,CAAA;AAEd,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAsB,CAAA;AA6CrD,MAAM,aAAa,GAAsB;IACvC,KAAK,EAAE,CAAC;IACR,UAAU,EAAE,KAAK;IACjB,QAAQ,EAAE,GAAG,EAAE,GAAE,CAAC;IAClB,eAAe,EAAE,GAAG,EAAE,GAAE,CAAC;IACzB,KAAK,EAAE,IAAI;IACX,UAAU,EAAE,GAAG,EAAE,GAAE,CAAC;CACrB,CAAA;AAED,MAAM,mBAAmB,GACvB,aAAa,CAAoB,aAAa,CAAC,CAAA;AAEjD,MAAM,CAAC,MAAM,eAAe,GAAG,GAAsB,EAAE,CACrD,UAAU,CAAC,mBAAmB,CAAC,CAAA;AAEjC,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,EACnC,QAAQ,GAGT,EAAa,EAAE;IACd,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAEpC,EAAE,CAAC,CAAA;IAEL,mEAAmE;IACnE,uBAAuB;IACvB,MAAM,UAAU,GAAG,YAAY,CAAC,OAAO,CAAC,CAAA;IAExC,MAAM,QAAQ,GAAG,WAAW,CAC1B,CAAC,KAAwB,EAAE,EAAE;QAC3B,UAAU,CAAC,CAAC,QAAQ,EAAE,EAAE;QACtB,yDAAyD;QACzD,0DAA0D;QAC1D,8DAA8D;QAC9D,yCAAyC;QACzC,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,KAAK,CAAC,EAAE,CAAC;YACzC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CACnB,GAAG,CAAC,EAAE,KAAK,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAClC;YACH,CAAC,CAAC,CAAC,GAAG,QAAQ,EAAE,KAAK,CAAC,CACzB,CAAA;IACH,CAAC,EACD,EAAE,CACH,CAAA;IAED,MAAM,UAAU,GAAG,WAAW,CAAC,CAAC,EAAU,EAAE,EAAE;QAC5C,UAAU,CAAC,CAAC,QAAQ,EAAE,EAAE,CACtB,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,CACxC,CAAA;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,eAAe,GAAG,WAAW,CAAC,GAAG,EAAE;QACvC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAA;IACtC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAA;IAEhB,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC;QACL,KAAK,EAAE,OAAO,CAAC,MAAM;QACrB,UAAU,EAAE,IAAI;QAChB,QAAQ;QACR,eAAe;QACf,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,IAAI;QACjC,UAAU;KACX,CAAC,EACF,CAAC,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,UAAU,CAAC,CACjD,CAAA;IAED,OAAO,CACL,MAAC,mBAAmB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,aAGxC,KAAC,cAAc,IAAC,SAAS,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,GAAI,EAEhD,QAAQ,IACoB,CAChC,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The scrim, and there is exactly one of it however many backdrop
|
|
4
|
+
* modals are stacked.
|
|
5
|
+
*
|
|
6
|
+
* Purely visual: `aria-hidden`, no handlers. Dismissal — a press
|
|
7
|
+
* anywhere off the top panel — is the top panel's own
|
|
8
|
+
* `useDismiss({ outsidePress })`, not a click on this node, which is
|
|
9
|
+
* what lets the whole package drop the `useKeyWithClickEvents`
|
|
10
|
+
* suppression the `<dialog>` backdrop needed. A press lands on the
|
|
11
|
+
* top panel's full-viewport centring layer (which sits above this),
|
|
12
|
+
* so this element never has to be the click target.
|
|
13
|
+
*
|
|
14
|
+
* Portalled to `document.body` on `--layer-overlay`, one step **below**
|
|
15
|
+
* the panels' `--layer-modal`, so it always paints behind every open
|
|
16
|
+
* modal and never intercepts a click meant for the dialog on top of it
|
|
17
|
+
* — a real bug a same-z-plus-DOM-order scheme has, because a
|
|
18
|
+
* later-mounted scrim then covers the panel. Stacking *between* panels
|
|
19
|
+
* still falls out of append order at the one `--layer-modal`.
|
|
20
|
+
*/
|
|
21
|
+
export declare const SharedBackdrop: ({ isVisible, }: {
|
|
22
|
+
isVisible: boolean;
|
|
23
|
+
}) => ReactNode;
|
|
24
|
+
//# sourceMappingURL=SharedBackdrop.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SharedBackdrop.d.ts","sourceRoot":"","sources":["../../src/Overlay/SharedBackdrop.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,cAAc,GAAI,gBAE5B;IACD,SAAS,EAAE,OAAO,CAAA;CACnB,KAAG,SAaH,CAAA"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { FloatingPortal } from "@floating-ui/react";
|
|
3
|
+
/**
|
|
4
|
+
* The scrim, and there is exactly one of it however many backdrop
|
|
5
|
+
* modals are stacked.
|
|
6
|
+
*
|
|
7
|
+
* Purely visual: `aria-hidden`, no handlers. Dismissal — a press
|
|
8
|
+
* anywhere off the top panel — is the top panel's own
|
|
9
|
+
* `useDismiss({ outsidePress })`, not a click on this node, which is
|
|
10
|
+
* what lets the whole package drop the `useKeyWithClickEvents`
|
|
11
|
+
* suppression the `<dialog>` backdrop needed. A press lands on the
|
|
12
|
+
* top panel's full-viewport centring layer (which sits above this),
|
|
13
|
+
* so this element never has to be the click target.
|
|
14
|
+
*
|
|
15
|
+
* Portalled to `document.body` on `--layer-overlay`, one step **below**
|
|
16
|
+
* the panels' `--layer-modal`, so it always paints behind every open
|
|
17
|
+
* modal and never intercepts a click meant for the dialog on top of it
|
|
18
|
+
* — a real bug a same-z-plus-DOM-order scheme has, because a
|
|
19
|
+
* later-mounted scrim then covers the panel. Stacking *between* panels
|
|
20
|
+
* still falls out of append order at the one `--layer-modal`.
|
|
21
|
+
*/
|
|
22
|
+
export const SharedBackdrop = ({ isVisible, }) => {
|
|
23
|
+
if (!isVisible) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return (_jsx(FloatingPortal, { children: _jsx("div", { "aria-hidden": "true", className: "fixed inset-0 z-[var(--layer-overlay)] bg-scrim" }) }));
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=SharedBackdrop.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SharedBackdrop.js","sourceRoot":"","sources":["../../src/Overlay/SharedBackdrop.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAGnD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,EAC7B,SAAS,GAGV,EAAa,EAAE;IACd,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,IAAI,CAAA;IACb,CAAC;IAED,OAAO,CACL,KAAC,cAAc,cACb,6BACc,MAAM,EAClB,SAAS,EAAC,iDAAiD,GAC3D,GACa,CAClB,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lockScrollBehind.d.ts","sourceRoot":"","sources":["../../src/Overlay/lockScrollBehind.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAMH,eAAO,MAAM,gBAAgB,QAAO,CAAC,MAAM,IAAI,CAiC9C,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lockScrollBehind.js","sourceRoot":"","sources":["../../src/Overlay/lockScrollBehind.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,IAAI,SAAS,GAAG,CAAC,CAAA;AAEjB,IAAI,gBAAgB,GAAG,EAAE,CAAA;AAEzB,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAiB,EAAE;IACjD,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;QACpC,OAAO,GAAG,EAAE,GAAE,CAAC,CAAA;IACjB,CAAC;IAED,MAAM,EAAE,eAAe,EAAE,GAAG,QAAQ,CAAA;IAEpC,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;QACpB,gBAAgB,GAAG,eAAe,CAAC,KAAK,CAAC,QAAQ,CAAA;QAEjD,eAAe,CAAC,KAAK,CAAC,QAAQ,GAAG,QAAQ,CAAA;IAC3C,CAAC;IAED,SAAS,IAAI,CAAC,CAAA;IAEd,IAAI,UAAU,GAAG,KAAK,CAAA;IAEtB,OAAO,GAAG,EAAE;QACV,6DAA6D;QAC7D,6DAA6D;QAC7D,qCAAqC;QACrC,IAAI,UAAU,EAAE,CAAC;YACf,OAAM;QACR,CAAC;QAED,UAAU,GAAG,IAAI,CAAA;QAEjB,SAAS,IAAI,CAAC,CAAA;QAEd,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;YACpB,eAAe,CAAC,KAAK,CAAC,QAAQ,GAAG,gBAAgB,CAAA;QACnD,CAAC;IACH,CAAC,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The surface an anchored overlay's panel is drawn on — the four
|
|
3
|
+
* tokens `Popover` and `Menu` wrote out byte-identically, and that
|
|
4
|
+
* `Listbox` and `Combobox` are about to write a third and fourth
|
|
5
|
+
* time.
|
|
6
|
+
*
|
|
7
|
+
* Only the *surface* lives here: the raised background, the hairline
|
|
8
|
+
* border, the corner radius, and the elevation. Everything a panel
|
|
9
|
+
* does with that surface — a popover's `max-w-xs p-3`, a menu's
|
|
10
|
+
* `min-w-48 flex flex-col`, a listbox's scroll clamp — is the panel's
|
|
11
|
+
* own and stays at the call site, composed after this with
|
|
12
|
+
* `toClassName`. Pulling the shared half out is what keeps a change
|
|
13
|
+
* to the overlay surface (a new elevation token, say) from having to
|
|
14
|
+
* be made in four places and kept in agreement by hand.
|
|
15
|
+
*/
|
|
16
|
+
export declare const PANEL_SURFACE_CLASS = "rounded-md border border-border-default bg-surface-overlay shadow-medium";
|
|
17
|
+
//# sourceMappingURL=overlayPanelClass.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overlayPanelClass.d.ts","sourceRoot":"","sources":["../../src/Overlay/overlayPanelClass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,6EAC4C,CAAA"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The surface an anchored overlay's panel is drawn on — the four
|
|
3
|
+
* tokens `Popover` and `Menu` wrote out byte-identically, and that
|
|
4
|
+
* `Listbox` and `Combobox` are about to write a third and fourth
|
|
5
|
+
* time.
|
|
6
|
+
*
|
|
7
|
+
* Only the *surface* lives here: the raised background, the hairline
|
|
8
|
+
* border, the corner radius, and the elevation. Everything a panel
|
|
9
|
+
* does with that surface — a popover's `max-w-xs p-3`, a menu's
|
|
10
|
+
* `min-w-48 flex flex-col`, a listbox's scroll clamp — is the panel's
|
|
11
|
+
* own and stays at the call site, composed after this with
|
|
12
|
+
* `toClassName`. Pulling the shared half out is what keeps a change
|
|
13
|
+
* to the overlay surface (a new elevation token, say) from having to
|
|
14
|
+
* be made in four places and kept in agreement by hand.
|
|
15
|
+
*/
|
|
16
|
+
export const PANEL_SURFACE_CLASS = "rounded-md border border-border-default bg-surface-overlay shadow-medium";
|
|
17
|
+
//# sourceMappingURL=overlayPanelClass.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overlayPanelClass.js","sourceRoot":"","sources":["../../src/Overlay/overlayPanelClass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAC9B,0EAA0E,CAAA"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Placement } from "@floating-ui/react";
|
|
2
|
+
import type { ReactElement } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* The floating-ui block every anchored overlay — `Popover`, `Menu`,
|
|
5
|
+
* `Listbox`, `Combobox` — wrote out for itself, consolidated once.
|
|
6
|
+
*
|
|
7
|
+
* `Popover` and `Menu` had the same twelve-line `useFloating` +
|
|
8
|
+
* `useInteractions` + `useClonedChild` preamble with two constants
|
|
9
|
+
* different (the offset and the role), and the fleet's four pickers
|
|
10
|
+
* hand-rolled a fifth, sixth, seventh and eighth `computePosition`
|
|
11
|
+
* that reimplemented `flip`/`shift` against `window.innerHeight`.
|
|
12
|
+
* This is the one implementation they all take.
|
|
13
|
+
*
|
|
14
|
+
* ### The owner's "configurable, but those are the basics"
|
|
15
|
+
*
|
|
16
|
+
* Escape and outside-press both close, **on by default**, for every
|
|
17
|
+
* anchored overlay — that is what an overlay is. A component switches
|
|
18
|
+
* one off through `dismiss` (a `Combobox`'s Escape clears the query
|
|
19
|
+
* before it closes; a menu opened from a context click wants no
|
|
20
|
+
* outside-press race with the thing that spawned it), but it opts out
|
|
21
|
+
* rather than opting in.
|
|
22
|
+
*
|
|
23
|
+
* ### `size`, for the pickers
|
|
24
|
+
*
|
|
25
|
+
* `matchTriggerWidth` and `maxHeightPx` are the two things
|
|
26
|
+
* `PortalDropdown` did with a hand-measured `TriggerRect` and a
|
|
27
|
+
* `window.innerHeight` clamp. The `size` middleware does both from the
|
|
28
|
+
* platform: the panel takes the trigger's width, and it never grows
|
|
29
|
+
* past the space actually left in the viewport.
|
|
30
|
+
*
|
|
31
|
+
* The computed width/height are written **straight onto the floating
|
|
32
|
+
* element's style** rather than lifted into React state, which is
|
|
33
|
+
* floating-ui's own documented shape and is deliberate twice over: a
|
|
34
|
+
* `setState` inside `apply` re-runs the position, which re-runs
|
|
35
|
+
* `apply`, which is a render loop; and because these properties never
|
|
36
|
+
* enter the `floatingStyles` object React manages, a component
|
|
37
|
+
* re-render (a keystroke in a combobox) does not wipe them — only an
|
|
38
|
+
* `autoUpdate` tick recomputes them.
|
|
39
|
+
*
|
|
40
|
+
* ### Portal-agnostic
|
|
41
|
+
*
|
|
42
|
+
* This returns the pieces — `clonedTrigger`, `getFloatingProps`,
|
|
43
|
+
* `setFloating`, `floatingStyles`, `context` — and renders nothing.
|
|
44
|
+
* Whether the panel goes in a `FloatingPortal`, the top layer, or in
|
|
45
|
+
* place is the component's call, which is what lets the same hook back
|
|
46
|
+
* a portalled `Listbox` and (for now) a top-layer `Popover`.
|
|
47
|
+
*/
|
|
48
|
+
export type AnchoredOverlayRole = "dialog" | "listbox" | "menu";
|
|
49
|
+
export type UseAnchoredOverlayOptions = {
|
|
50
|
+
/**
|
|
51
|
+
* Both default **on** — Escape and outside-press are what makes an
|
|
52
|
+
* overlay an overlay. Switch one off for the honest exception (a
|
|
53
|
+
* combobox that swallows the first Escape to clear its query), never
|
|
54
|
+
* to make a plain overlay sticky. Named for the flattened
|
|
55
|
+
* floating-ui `useDismiss` options they carry.
|
|
56
|
+
*/
|
|
57
|
+
isEscapeDismissable?: boolean;
|
|
58
|
+
isOutsidePressDismissable?: boolean;
|
|
59
|
+
/** Ports `PortalDropdown`'s anchor-width match, via `size`. */
|
|
60
|
+
isTriggerWidthMatched?: boolean;
|
|
61
|
+
isVisible: boolean;
|
|
62
|
+
/** Clamps the panel to `min(this, available viewport space)`. */
|
|
63
|
+
maxHeightPx?: number;
|
|
64
|
+
/** Popover 8, Menu/Listbox/Combobox 4. */
|
|
65
|
+
offsetValue?: number;
|
|
66
|
+
onDismiss: () => void;
|
|
67
|
+
placement?: Placement;
|
|
68
|
+
role: AnchoredOverlayRole;
|
|
69
|
+
/** The control the panel hangs off. **Cloned, not wrapped.** */
|
|
70
|
+
trigger: ReactElement;
|
|
71
|
+
};
|
|
72
|
+
export declare const useAnchoredOverlay: ({ isEscapeDismissable, isOutsidePressDismissable, isTriggerWidthMatched, isVisible, maxHeightPx, offsetValue, onDismiss, placement, role, trigger, }: UseAnchoredOverlayOptions) => {
|
|
73
|
+
clonedTrigger: ReactElement<Record<string, unknown>, string | import("react").JSXElementConstructor<any>>;
|
|
74
|
+
context: {
|
|
75
|
+
placement: Placement;
|
|
76
|
+
strategy: import("@floating-ui/utils").Strategy;
|
|
77
|
+
x: number;
|
|
78
|
+
y: number;
|
|
79
|
+
middlewareData: import("@floating-ui/core").MiddlewareData;
|
|
80
|
+
isPositioned: boolean;
|
|
81
|
+
update: () => void;
|
|
82
|
+
floatingStyles: React.CSSProperties;
|
|
83
|
+
open: boolean;
|
|
84
|
+
onOpenChange: (open: boolean, event?: Event, reason?: import("@floating-ui/react").OpenChangeReason) => void;
|
|
85
|
+
events: import("@floating-ui/react").FloatingEvents;
|
|
86
|
+
dataRef: React.MutableRefObject<import("@floating-ui/react").ContextData>;
|
|
87
|
+
nodeId: string | undefined;
|
|
88
|
+
floatingId: string | undefined;
|
|
89
|
+
refs: import("@floating-ui/react").ExtendedRefs<import("@floating-ui/react").ReferenceType>;
|
|
90
|
+
elements: import("@floating-ui/react").ExtendedElements<import("@floating-ui/react").ReferenceType>;
|
|
91
|
+
};
|
|
92
|
+
floatingStyles: import("react").CSSProperties;
|
|
93
|
+
getFloatingProps: (userProps?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
|
|
94
|
+
setFloating: ((node: HTMLElement | null) => void) & ((node: HTMLElement | null) => void);
|
|
95
|
+
triggerId: string;
|
|
96
|
+
};
|
|
97
|
+
//# sourceMappingURL=useAnchoredOverlay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAnchoredOverlay.d.ts","sourceRoot":"","sources":["../../src/Overlay/useAnchoredOverlay.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAYnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAA;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,MAAM,MAAM,mBAAmB,GAC3B,QAAQ,GACR,SAAS,GACT,MAAM,CAAA;AAEV,MAAM,MAAM,yBAAyB,GAAG;IACtC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,yBAAyB,CAAC,EAAE,OAAO,CAAA;IACnC,+DAA+D;IAC/D,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,SAAS,EAAE,OAAO,CAAA;IAClB,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,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,gEAAgE;IAChE,OAAO,EAAE,YAAY,CAAA;CACtB,CAAA;AAED,eAAO,MAAM,kBAAkB,GAAI,sJAWhC,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;CA6F3B,CAAA"}
|