@charcuterie/ui 1.0.1 → 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/ColorSchemeSwitcher/ColorSchemeSwitcher.d.ts +35 -0
- package/dist/ColorSchemeSwitcher/ColorSchemeSwitcher.d.ts.map +1 -0
- package/dist/ColorSchemeSwitcher/ColorSchemeSwitcher.js +33 -0
- package/dist/ColorSchemeSwitcher/ColorSchemeSwitcher.js.map +1 -0
- package/dist/ColorSchemeToggle/ColorSchemeToggle.d.ts +54 -0
- package/dist/ColorSchemeToggle/ColorSchemeToggle.d.ts.map +1 -0
- package/dist/ColorSchemeToggle/ColorSchemeToggle.js +30 -0
- package/dist/ColorSchemeToggle/ColorSchemeToggle.js.map +1 -0
- 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 +57 -0
- package/dist/Lightbox/Lightbox.d.ts.map +1 -0
- package/dist/Lightbox/Lightbox.js +50 -0
- package/dist/Lightbox/Lightbox.js.map +1 -0
- 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 +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -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 +5 -4
- package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.mdx +70 -0
- package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.stories.tsx +118 -0
- package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.test.tsx +53 -0
- package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.tsx +90 -0
- package/src/ColorSchemeToggle/ColorSchemeToggle.mdx +48 -0
- package/src/ColorSchemeToggle/ColorSchemeToggle.stories.tsx +161 -0
- package/src/ColorSchemeToggle/ColorSchemeToggle.test.tsx +80 -0
- package/src/ColorSchemeToggle/ColorSchemeToggle.tsx +107 -0
- 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 +63 -0
- package/src/Lightbox/Lightbox.stories.tsx +133 -0
- package/src/Lightbox/Lightbox.test.tsx +124 -0
- package/src/Lightbox/Lightbox.tsx +143 -0
- 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/icons.storyHelpers.tsx +22 -0
- package/src/index.ts +22 -4
- package/src/mountStory.testHelpers.ts +25 -1
- package/src/sourceRules.test.ts +26 -1
- 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
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useUniqueId } from "@charcuterie/logic";
|
|
3
|
+
import { Button } from "../Button/Button.js";
|
|
4
|
+
import { Modal } from "../Modal/Modal.js";
|
|
5
|
+
import { toClassName } from "../toClassName.js";
|
|
6
|
+
/**
|
|
7
|
+
* `85dvh` rather than `85vh` — on a phone the two differ by the
|
|
8
|
+
* address bar, and a dialog whose footer is under it is a dialog
|
|
9
|
+
* with no buttons.
|
|
10
|
+
*/
|
|
11
|
+
const SIZE_CLASS = {
|
|
12
|
+
sm: "max-h-[85dvh] w-full max-w-sm",
|
|
13
|
+
md: "max-h-[85dvh] w-full max-w-md",
|
|
14
|
+
lg: "max-h-[85dvh] w-full max-w-2xl",
|
|
15
|
+
// M5 added this, because the scale jumped from 42rem straight to
|
|
16
|
+
// the whole viewport and rip-deck's capture tail lands in the
|
|
17
|
+
// gap: a MakeMKV robot log is fixed-width `MSG:5072,0,1,"…"`
|
|
18
|
+
// lines that wrap badly at 42rem, and a full-screen dialog over a
|
|
19
|
+
// nine-bay dashboard hides the thing being diagnosed. 56rem is
|
|
20
|
+
// what that app's hand-rolled `<dialog>` used before the swap.
|
|
21
|
+
xl: "max-h-[85dvh] w-full max-w-4xl",
|
|
22
|
+
full: "h-[100dvh] max-h-[100dvh] w-full max-w-full rounded-none",
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* `Modal` plus the chrome — heading, a Close button, a scrolling body,
|
|
26
|
+
* an optional footer, and five sizes. This is what `Modal` *was*
|
|
27
|
+
* before the M8 rebuild renamed it: a dialog is one kind of modal, so
|
|
28
|
+
* it takes the specific name and hands the generic one back to the
|
|
29
|
+
* base layer.
|
|
30
|
+
*
|
|
31
|
+
* Everything modal — the portal, the scrim, Escape and outside-press
|
|
32
|
+
* dismiss, the focus trap, the scroll lock, the stacking — is `Modal`'s
|
|
33
|
+
* (via `OverlayPanel`), so this component is just the box's contents
|
|
34
|
+
* and the size class it wants that box to be. `SIZE_CLASS` is composed
|
|
35
|
+
* over `Modal`'s default surface, which is why `full` carries
|
|
36
|
+
* `rounded-none` to square off the base's `rounded-lg` at the viewport
|
|
37
|
+
* edge.
|
|
38
|
+
*
|
|
39
|
+
* ### The accessible name
|
|
40
|
+
*
|
|
41
|
+
* `heading` is required and is a string, not a `ReactNode`. It names
|
|
42
|
+
* both the visible header and — through `aria-labelledby` on the
|
|
43
|
+
* modal — the dialog itself. A dialog with no name is announced as
|
|
44
|
+
* "dialog" and nothing else, which is what every hand-rolled modal in
|
|
45
|
+
* the inventory does.
|
|
46
|
+
*/
|
|
47
|
+
export const Dialog = ({ children, className, footer, heading, headingLevel = 2, isDismissable = true, isVisible, onClose, size = "md", }) => {
|
|
48
|
+
const headingId = useUniqueId();
|
|
49
|
+
const Heading = `h${headingLevel}`;
|
|
50
|
+
return (_jsxs(Modal, { "aria-labelledby": headingId, className: toClassName(SIZE_CLASS[size], className), isDismissable: isDismissable, isVisible: isVisible, onClose: onClose, 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] }));
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=Dialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Dialog.js","sourceRoot":"","sources":["../../src/Dialog/Dialog.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAGhD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAsB,CAAA;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAoB,CAAA;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAyB/C;;;;GAIG;AACH,MAAM,UAAU,GAA+B;IAC7C,EAAE,EAAE,+BAA+B;IACnC,EAAE,EAAE,+BAA+B;IACnC,EAAE,EAAE,gCAAgC;IACpC,iEAAiE;IACjE,8DAA8D;IAC9D,6DAA6D;IAC7D,kEAAkE;IAClE,+DAA+D;IAC/D,+DAA+D;IAC/D,EAAE,EAAE,gCAAgC;IACpC,IAAI,EAAE,0DAA0D;CACjE,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,EACrB,QAAQ,EACR,SAAS,EACT,MAAM,EACN,OAAO,EACP,YAAY,GAAG,CAAC,EAChB,aAAa,GAAG,IAAI,EACpB,SAAS,EACT,OAAO,EACP,IAAI,GAAG,IAAI,GACC,EAAa,EAAE;IAC3B,MAAM,SAAS,GAAG,WAAW,EAAE,CAAA;IAE/B,MAAM,OAAO,GAAG,IAAI,YAAY,EAAW,CAAA;IAE3C,OAAO,CACL,MAAC,KAAK,uBACa,SAAS,EAC1B,SAAS,EAAE,WAAW,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,EACnD,aAAa,EAAE,aAAa,EAC5B,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,OAAO,aAEhB,kBAAQ,SAAS,EAAC,mFAAmF,aACnG,KAAC,OAAO,IACN,SAAS,EAAC,qCAAqC,EAC/C,EAAE,EAAE,SAAS,YAEZ,OAAO,GACA,EAET,aAAa,CAAC,CAAC,CAAC,CACf,KAAC,MAAM,IACL,UAAU,EAAC,OAAO,EAClB,MAAM,EAAC,SAAS,EAChB,OAAO,EAAE,OAAO,EAChB,IAAI,EAAC,IAAI,sBAGF,CACV,CAAC,CAAC,CAAC,IAAI,IACD,EAET,cAAK,SAAS,EAAC,kCAAkC,YAC9C,QAAQ,GACL,EAEL,MAAM,CAAC,CAAC,CAAC,CACR,iBAAQ,SAAS,EAAC,6EAA6E,YAC5F,MAAM,GACA,CACV,CAAC,CAAC,CAAC,IAAI,IACF,CACT,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type LightboxProps = {
|
|
3
|
+
/**
|
|
4
|
+
* Required, and the whole reason this is not a bare `<img>` in a
|
|
5
|
+
* dialog. It names both the enlarged image and — via `heading`'s
|
|
6
|
+
* default below — the dialog itself, and a poster grid of unnamed
|
|
7
|
+
* images is unnavigable rather than merely imperfect. Same rule
|
|
8
|
+
* `MediaTile` makes.
|
|
9
|
+
*/
|
|
10
|
+
alt: string;
|
|
11
|
+
/** Shown under the enlarged image. A year, a source, a filename. */
|
|
12
|
+
caption?: ReactNode;
|
|
13
|
+
/** Applied to the built-in trigger button, not the dialog. */
|
|
14
|
+
className?: string;
|
|
15
|
+
/**
|
|
16
|
+
* The dialog's accessible name and visible heading. Defaults to
|
|
17
|
+
* `alt`, which is right when the thumbnail's alt is already the
|
|
18
|
+
* title ("THE OUTFIT poster"); pass it when the two should differ.
|
|
19
|
+
*/
|
|
20
|
+
heading?: string;
|
|
21
|
+
/**
|
|
22
|
+
* **Controlled** open state. Omit for the uncontrolled default,
|
|
23
|
+
* where the trigger owns it through `useVisibility`. Present means
|
|
24
|
+
* the caller owns it and the built-in trigger becomes a plain
|
|
25
|
+
* button that only *reports* intent through `onOpenChange`.
|
|
26
|
+
*/
|
|
27
|
+
isOpen?: boolean;
|
|
28
|
+
onOpenChange?: (isOpen: boolean) => void;
|
|
29
|
+
src: string;
|
|
30
|
+
/**
|
|
31
|
+
* The clickable thumbnail — a smaller `<img>`, usually. Omitted in
|
|
32
|
+
* the controlled, trigger-elsewhere case, where something else in
|
|
33
|
+
* the page opens the lightbox and this renders only the overlay.
|
|
34
|
+
*/
|
|
35
|
+
thumbnail?: ReactNode;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* A thumbnail that opens its own full-size view — the one image
|
|
39
|
+
* interaction three apps hand-roll and none get the focus trap or
|
|
40
|
+
* the scroll lock right. It is a **skin over `Dialog`**, not a second
|
|
41
|
+
* dialog: `Dialog` (through `Modal`/`OverlayPanel`) owns the portal,
|
|
42
|
+
* the scrim, Escape, the focus trap and `lockScrollBehind`, and
|
|
43
|
+
* everything that makes those correct in every browser the fleet
|
|
44
|
+
* runs. What this adds is the image-specific part — a `cursor-zoom-in`
|
|
45
|
+
* trigger and an `object-contain` image clamped to the viewport so a
|
|
46
|
+
* 2:3 poster and a 16:9 still both land whole.
|
|
47
|
+
*
|
|
48
|
+
* ### Controlled and not, one component
|
|
49
|
+
*
|
|
50
|
+
* `useVisibility` runs unconditionally — hooks cannot be conditional
|
|
51
|
+
* — but is *consulted* only when `isOpen` is absent. A caller that
|
|
52
|
+
* passes `isOpen` owns the truth; the internal store is then an
|
|
53
|
+
* unread two-field object, which is cheaper than splitting this into
|
|
54
|
+
* two components that share a Modal body.
|
|
55
|
+
*/
|
|
56
|
+
export declare const Lightbox: ({ alt, caption, className, heading, isOpen, onOpenChange, src, thumbnail, }: LightboxProps) => ReactNode;
|
|
57
|
+
//# sourceMappingURL=Lightbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Lightbox.d.ts","sourceRoot":"","sources":["../../src/Lightbox/Lightbox.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;;;OAMG;IACH,GAAG,EAAE,MAAM,CAAA;IACX,oEAAoE;IACpE,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAA;IACxC,GAAG,EAAE,MAAM,CAAA;IACX;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB,CAAA;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,QAAQ,GAAI,6EAStB,aAAa,KAAG,SAwElB,CAAA"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useVisibility } from "@charcuterie/logic";
|
|
3
|
+
import { Dialog } from "../Dialog/Dialog.js";
|
|
4
|
+
import { toClassName } from "../toClassName.js";
|
|
5
|
+
/**
|
|
6
|
+
* A thumbnail that opens its own full-size view — the one image
|
|
7
|
+
* interaction three apps hand-roll and none get the focus trap or
|
|
8
|
+
* the scroll lock right. It is a **skin over `Dialog`**, not a second
|
|
9
|
+
* dialog: `Dialog` (through `Modal`/`OverlayPanel`) owns the portal,
|
|
10
|
+
* the scrim, Escape, the focus trap and `lockScrollBehind`, and
|
|
11
|
+
* everything that makes those correct in every browser the fleet
|
|
12
|
+
* runs. What this adds is the image-specific part — a `cursor-zoom-in`
|
|
13
|
+
* trigger and an `object-contain` image clamped to the viewport so a
|
|
14
|
+
* 2:3 poster and a 16:9 still both land whole.
|
|
15
|
+
*
|
|
16
|
+
* ### Controlled and not, one component
|
|
17
|
+
*
|
|
18
|
+
* `useVisibility` runs unconditionally — hooks cannot be conditional
|
|
19
|
+
* — but is *consulted* only when `isOpen` is absent. A caller that
|
|
20
|
+
* passes `isOpen` owns the truth; the internal store is then an
|
|
21
|
+
* unread two-field object, which is cheaper than splitting this into
|
|
22
|
+
* two components that share a Modal body.
|
|
23
|
+
*/
|
|
24
|
+
export const Lightbox = ({ alt, caption, className, heading, isOpen, onOpenChange, src, thumbnail, }) => {
|
|
25
|
+
const isControlled = isOpen !== undefined;
|
|
26
|
+
const { hide, isVisible, show } = useVisibility();
|
|
27
|
+
const isShown = isControlled ? isOpen : isVisible;
|
|
28
|
+
const requestOpen = () => {
|
|
29
|
+
if (!isControlled) {
|
|
30
|
+
show();
|
|
31
|
+
}
|
|
32
|
+
onOpenChange?.(true);
|
|
33
|
+
};
|
|
34
|
+
const requestClose = () => {
|
|
35
|
+
if (!isControlled) {
|
|
36
|
+
hide();
|
|
37
|
+
}
|
|
38
|
+
onOpenChange?.(false);
|
|
39
|
+
};
|
|
40
|
+
return (_jsxs(_Fragment, { children: [thumbnail === undefined ? null : (_jsx("button", { "aria-label": `Enlarge ${heading ?? alt}`, className: toClassName(
|
|
41
|
+
// `cursor-zoom-in` is the affordance a bare `<img>`
|
|
42
|
+
// never had, and the focus ring is the fleet's standard
|
|
43
|
+
// — the same one `MediaTile`'s link wears.
|
|
44
|
+
"group block cursor-zoom-in rounded-md outline-offset-2 focus-visible:outline-solid focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-focus-ring", className), onClick: requestOpen, type: "button", children: thumbnail })), _jsx(Dialog, { heading: heading ?? alt, isVisible: isShown, onClose: requestClose, size: "xl", children: _jsxs("figure", { className: "flex flex-col items-center gap-3", children: [_jsx("img", { alt: alt,
|
|
45
|
+
// `object-contain`, not `cover`: the enlarged view must
|
|
46
|
+
// show the whole image, and the clamp is `dvh` for the
|
|
47
|
+
// same address-bar reason `Modal`'s is.
|
|
48
|
+
className: "max-h-[75dvh] w-auto max-w-full rounded-md object-contain", src: src }), caption ? (_jsx("figcaption", { className: "text-center text-content-muted text-sm", children: caption })) : null] }) })] }));
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=Lightbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Lightbox.js","sourceRoot":"","sources":["../../src/Lightbox/Lightbox.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAGlD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAsB,CAAA;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAsC/C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,EACvB,GAAG,EACH,OAAO,EACP,SAAS,EACT,OAAO,EACP,MAAM,EACN,YAAY,EACZ,GAAG,EACH,SAAS,GACK,EAAa,EAAE;IAC7B,MAAM,YAAY,GAAG,MAAM,KAAK,SAAS,CAAA;IAEzC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,aAAa,EAAE,CAAA;IAEjD,MAAM,OAAO,GAAG,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAA;IAEjD,MAAM,WAAW,GAAG,GAAG,EAAE;QACvB,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,IAAI,EAAE,CAAA;QACR,CAAC;QAED,YAAY,EAAE,CAAC,IAAI,CAAC,CAAA;IACtB,CAAC,CAAA;IAED,MAAM,YAAY,GAAG,GAAG,EAAE;QACxB,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,IAAI,EAAE,CAAA;QACR,CAAC;QAED,YAAY,EAAE,CAAC,KAAK,CAAC,CAAA;IACvB,CAAC,CAAA;IAED,OAAO,CACL,8BACG,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAChC,+BAMc,WAAW,OAAO,IAAI,GAAG,EAAE,EACvC,SAAS,EAAE,WAAW;gBACpB,oDAAoD;gBACpD,wDAAwD;gBACxD,2CAA2C;gBAC3C,uKAAuK,EACvK,SAAS,CACV,EACD,OAAO,EAAE,WAAW,EACpB,IAAI,EAAC,QAAQ,YAEZ,SAAS,GACH,CACV,EAED,KAAC,MAAM,IACL,OAAO,EAAE,OAAO,IAAI,GAAG,EACvB,SAAS,EAAE,OAAO,EAClB,OAAO,EAAE,YAAY,EACrB,IAAI,EAAC,IAAI,YAET,kBAAQ,SAAS,EAAC,kCAAkC,aAClD,cACE,GAAG,EAAE,GAAG;4BACR,wDAAwD;4BACxD,uDAAuD;4BACvD,wCAAwC;4BACxC,SAAS,EAAC,2DAA2D,EACrE,GAAG,EAAE,GAAG,GACR,EAED,OAAO,CAAC,CAAC,CAAC,CACT,qBAAY,SAAS,EAAC,wCAAwC,YAC3D,OAAO,GACG,CACd,CAAC,CAAC,CAAC,IAAI,IACD,GACF,IACR,CACJ,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Placement } from "@floating-ui/react";
|
|
2
|
+
import type { ReactElement, ReactNode } from "react";
|
|
3
|
+
export type ListboxItem = {
|
|
4
|
+
isDisabled?: boolean;
|
|
5
|
+
label: ReactNode;
|
|
6
|
+
/**
|
|
7
|
+
* Required when `label` is rich (an icon, two lines, a badge): it is
|
|
8
|
+
* the type-ahead target and the accessible name a screen reader
|
|
9
|
+
* reads. When `label` is a plain string it is used as the fallback.
|
|
10
|
+
*/
|
|
11
|
+
textValue?: string;
|
|
12
|
+
value: string;
|
|
13
|
+
};
|
|
14
|
+
export type ListboxProps = {
|
|
15
|
+
className?: string;
|
|
16
|
+
isVisible: boolean;
|
|
17
|
+
/** Outside press and Escape both land here. */
|
|
18
|
+
onDismiss: () => void;
|
|
19
|
+
onSelect: (value: string) => void;
|
|
20
|
+
options: readonly ListboxItem[];
|
|
21
|
+
placement?: Placement;
|
|
22
|
+
/** The initial seed. The live selection is the listbox's after that. */
|
|
23
|
+
selectedValue?: string;
|
|
24
|
+
/** The control the listbox hangs off. **Cloned, not wrapped.** */
|
|
25
|
+
trigger: ReactElement;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The single-select fancy dropdown — the rich-option sibling of the
|
|
29
|
+
* native `Select`. `Select` keeps type-ahead, the mobile wheel, form
|
|
30
|
+
* submission and autofill; `Listbox` is for the cases `<option>`
|
|
31
|
+
* cannot render: an icon, two lines, a trailing badge.
|
|
32
|
+
*
|
|
33
|
+
* A listbox is **not** a menu, and that distinction is the whole
|
|
34
|
+
* reason it is a separate component from `Menu`. An `option` *is*
|
|
35
|
+
* something you are choosing — so it carries `aria-selected`, it
|
|
36
|
+
* type-aheads (which the APG requires of a listbox and `Menu`
|
|
37
|
+
* deliberately omits), and a screen reader announces "listbox,
|
|
38
|
+
* selected, 2 of 4". A `menuitem` only ever *does* something.
|
|
39
|
+
*
|
|
40
|
+
* ### Two kinds of state, composed
|
|
41
|
+
*
|
|
42
|
+
* `useRovingFocus` owns which option is tabbable (exactly one, so Tab
|
|
43
|
+
* enters and leaves the group once and the arrow keys move within it),
|
|
44
|
+
* and `useSinglePicker` owns which option is chosen. They are the same
|
|
45
|
+
* two kinds `Tabs` composes, and they never disturb each other:
|
|
46
|
+
* arrowing moves focus without selecting, Enter selects the focused
|
|
47
|
+
* option.
|
|
48
|
+
*
|
|
49
|
+
* ### Portalled, and named by its trigger
|
|
50
|
+
*
|
|
51
|
+
* Like `Menu`, the panel portals to `document.body` and `useRole` names
|
|
52
|
+
* it by pointing `aria-labelledby` at the trigger — so there is no
|
|
53
|
+
* `heading` prop; the button that opens it is its label.
|
|
54
|
+
*/
|
|
55
|
+
export declare const Listbox: ({ className, isVisible, onDismiss, onSelect, options, placement, selectedValue, trigger, }: ListboxProps) => ReactNode;
|
|
56
|
+
//# sourceMappingURL=Listbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Listbox.d.ts","sourceRoot":"","sources":["../../src/Listbox/Listbox.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAKnD,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAQpD,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,KAAK,EAAE,SAAS,CAAA;IAChB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,OAAO,CAAA;IAClB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAA;IAC/B,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,kEAAkE;IAClE,OAAO,EAAE,YAAY,CAAA;CACtB,CAAA;AAQD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,OAAO,GAAI,4FASrB,YAAY,KAAG,SA+MjB,CAAA"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { selectTabIndex, useRovingFocus, useSinglePicker, } from "@charcuterie/logic";
|
|
3
|
+
import { FloatingFocusManager, FloatingPortal, } from "@floating-ui/react";
|
|
4
|
+
import { useEffect, useRef } from "react";
|
|
5
|
+
import { PANEL_SURFACE_CLASS } from "../Overlay/overlayPanelClass.js";
|
|
6
|
+
import { useAnchoredOverlay } from "../Overlay/useAnchoredOverlay.js";
|
|
7
|
+
import { toClassName } from "../toClassName.js";
|
|
8
|
+
import { ListboxOption } from "./ListboxOption.js";
|
|
9
|
+
const getOptionText = (item) => (item.textValue ??
|
|
10
|
+
(typeof item.label === "string" ? item.label : "")).toLowerCase();
|
|
11
|
+
/**
|
|
12
|
+
* The single-select fancy dropdown — the rich-option sibling of the
|
|
13
|
+
* native `Select`. `Select` keeps type-ahead, the mobile wheel, form
|
|
14
|
+
* submission and autofill; `Listbox` is for the cases `<option>`
|
|
15
|
+
* cannot render: an icon, two lines, a trailing badge.
|
|
16
|
+
*
|
|
17
|
+
* A listbox is **not** a menu, and that distinction is the whole
|
|
18
|
+
* reason it is a separate component from `Menu`. An `option` *is*
|
|
19
|
+
* something you are choosing — so it carries `aria-selected`, it
|
|
20
|
+
* type-aheads (which the APG requires of a listbox and `Menu`
|
|
21
|
+
* deliberately omits), and a screen reader announces "listbox,
|
|
22
|
+
* selected, 2 of 4". A `menuitem` only ever *does* something.
|
|
23
|
+
*
|
|
24
|
+
* ### Two kinds of state, composed
|
|
25
|
+
*
|
|
26
|
+
* `useRovingFocus` owns which option is tabbable (exactly one, so Tab
|
|
27
|
+
* enters and leaves the group once and the arrow keys move within it),
|
|
28
|
+
* and `useSinglePicker` owns which option is chosen. They are the same
|
|
29
|
+
* two kinds `Tabs` composes, and they never disturb each other:
|
|
30
|
+
* arrowing moves focus without selecting, Enter selects the focused
|
|
31
|
+
* option.
|
|
32
|
+
*
|
|
33
|
+
* ### Portalled, and named by its trigger
|
|
34
|
+
*
|
|
35
|
+
* Like `Menu`, the panel portals to `document.body` and `useRole` names
|
|
36
|
+
* it by pointing `aria-labelledby` at the trigger — so there is no
|
|
37
|
+
* `heading` prop; the button that opens it is its label.
|
|
38
|
+
*/
|
|
39
|
+
export const Listbox = ({ className, isVisible, onDismiss, onSelect, options, placement = "bottom-start", selectedValue, trigger, }) => {
|
|
40
|
+
const optionElements = useRef(new Map());
|
|
41
|
+
const typeahead = useRef({ buffer: "", stampMs: 0 });
|
|
42
|
+
const focus = useRovingFocus();
|
|
43
|
+
const picker = useSinglePicker({ selectedValue });
|
|
44
|
+
const { activeValue, registeredValues, setActiveValue } = focus;
|
|
45
|
+
const [firstValue] = registeredValues;
|
|
46
|
+
const { clonedTrigger, context, floatingStyles, getFloatingProps, setFloating, triggerId, } = useAnchoredOverlay({
|
|
47
|
+
isVisible,
|
|
48
|
+
maxHeightPx: 384,
|
|
49
|
+
offsetValue: 4,
|
|
50
|
+
onDismiss,
|
|
51
|
+
placement,
|
|
52
|
+
role: "listbox",
|
|
53
|
+
trigger,
|
|
54
|
+
});
|
|
55
|
+
const chooseValue = (value) => {
|
|
56
|
+
picker.select(value);
|
|
57
|
+
onSelect(value);
|
|
58
|
+
// A single choice closes, the way the native `<select>` it stands
|
|
59
|
+
// in for does — the consumer's `onDismiss` is the same one an
|
|
60
|
+
// outside press and Escape use.
|
|
61
|
+
onDismiss();
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Opening lands the focus on the chosen option, or the first when
|
|
65
|
+
* nothing is chosen yet — the APG rule for a listbox, and the reason
|
|
66
|
+
* a reopened one resumes on the current value rather than the top.
|
|
67
|
+
*/
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!isVisible) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const seed = picker.selectedValue ?? firstValue;
|
|
73
|
+
if (seed !== undefined && seed !== null) {
|
|
74
|
+
setActiveValue(seed);
|
|
75
|
+
}
|
|
76
|
+
}, [
|
|
77
|
+
firstValue,
|
|
78
|
+
isVisible,
|
|
79
|
+
picker.selectedValue,
|
|
80
|
+
setActiveValue,
|
|
81
|
+
]);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!isVisible || activeValue === null) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
optionElements.current.get(activeValue)?.focus();
|
|
87
|
+
}, [activeValue, isVisible]);
|
|
88
|
+
const runTypeahead = (character) => {
|
|
89
|
+
// Reset the buffer once the typing pauses, so "sk" then a beat
|
|
90
|
+
// then "e" is two searches rather than "ske".
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
typeahead.current.buffer =
|
|
93
|
+
now - typeahead.current.stampMs > 500
|
|
94
|
+
? character
|
|
95
|
+
: typeahead.current.buffer + character;
|
|
96
|
+
typeahead.current.stampMs = now;
|
|
97
|
+
const query = typeahead.current.buffer.toLowerCase();
|
|
98
|
+
const match = options.find((option) => !option.isDisabled &&
|
|
99
|
+
getOptionText(option).startsWith(query));
|
|
100
|
+
if (match) {
|
|
101
|
+
setActiveValue(match.value);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
return (_jsxs(_Fragment, { children: [clonedTrigger, isVisible ? (_jsx(FloatingPortal, { children: _jsx(FloatingFocusManager, { context: context, initialFocus: -1, modal: false, children: _jsx("div", { ...getFloatingProps(), "aria-labelledby": triggerId, className: toClassName(PANEL_SURFACE_CLASS,
|
|
105
|
+
// The height is clamped to the viewport by the `size`
|
|
106
|
+
// middleware (`maxHeightPx`), written as an inline
|
|
107
|
+
// style; this scrolls within it.
|
|
108
|
+
"z-[var(--layer-modal)] flex min-w-48 flex-col gap-0.5 overflow-auto p-1", className), onKeyDown: (keyEvent) => {
|
|
109
|
+
const commands = {
|
|
110
|
+
ArrowDown: focus.next,
|
|
111
|
+
ArrowUp: focus.previous,
|
|
112
|
+
End: focus.last,
|
|
113
|
+
Home: focus.first,
|
|
114
|
+
};
|
|
115
|
+
const command = commands[keyEvent.key];
|
|
116
|
+
if (command) {
|
|
117
|
+
keyEvent.preventDefault();
|
|
118
|
+
command();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if ((keyEvent.key === "Enter" ||
|
|
122
|
+
keyEvent.key === " ") &&
|
|
123
|
+
activeValue !== null) {
|
|
124
|
+
keyEvent.preventDefault();
|
|
125
|
+
chooseValue(activeValue);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Type-ahead: a single printable character with no
|
|
129
|
+
// modifier is a search, not a shortcut.
|
|
130
|
+
if (keyEvent.key.length === 1 &&
|
|
131
|
+
!keyEvent.altKey &&
|
|
132
|
+
!keyEvent.ctrlKey &&
|
|
133
|
+
!keyEvent.metaKey) {
|
|
134
|
+
runTypeahead(keyEvent.key);
|
|
135
|
+
}
|
|
136
|
+
}, ref: setFloating,
|
|
137
|
+
// Duplicated from `getFloatingProps()` on purpose, and
|
|
138
|
+
// the two cannot differ — `useRole(context, { role:
|
|
139
|
+
// "listbox" })` in the hook is where it comes from.
|
|
140
|
+
// Stated here because a linter cannot see a role through
|
|
141
|
+
// a spread.
|
|
142
|
+
role: "listbox", style: floatingStyles, children: options.map((option) => (_jsx(ListboxOption, { isSelected: picker.selectedValue === option.value, item: option, onSelect: chooseValue, registerFocus: focus.register, registerSelection: picker.register, tabIndex: selectTabIndex(focus, option.value), trackElement: (value, element) => {
|
|
143
|
+
if (element) {
|
|
144
|
+
optionElements.current.set(value, element);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
optionElements.current.delete(value);
|
|
148
|
+
}
|
|
149
|
+
} }, option.value))) }) }) })) : null] }));
|
|
150
|
+
};
|
|
151
|
+
//# sourceMappingURL=Listbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Listbox.js","sourceRoot":"","sources":["../../src/Listbox/Listbox.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ListboxItem } from "./Listbox.tsx";
|
|
3
|
+
export type ListboxOptionProps = {
|
|
4
|
+
isSelected: boolean;
|
|
5
|
+
item: ListboxItem;
|
|
6
|
+
onSelect: (value: string) => void;
|
|
7
|
+
/** `RovingFocus.register` — membership of the arrow-key group. */
|
|
8
|
+
registerFocus: (value: string) => () => void;
|
|
9
|
+
/** `SinglePicker.register` — membership of the choice. */
|
|
10
|
+
registerSelection: (value: string) => () => void;
|
|
11
|
+
tabIndex: number;
|
|
12
|
+
trackElement: (value: string, element: HTMLButtonElement | null) => void;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Its own file for the same reason as `MenuAction` and `TabTrigger`:
|
|
16
|
+
* **both registrations are effects**, and an effect cannot run in a
|
|
17
|
+
* loop.
|
|
18
|
+
*
|
|
19
|
+
* The two registrations are separate on purpose, exactly as
|
|
20
|
+
* `TabTrigger` splits them. A disabled option joins the *choice* — it
|
|
21
|
+
* can still be the seeded `selectedValue` and still announces
|
|
22
|
+
* `aria-selected` — and stays out of the *focus* group, so the arrow
|
|
23
|
+
* keys and type-ahead skip it without `RovingFocus` having to know
|
|
24
|
+
* what "disabled" means.
|
|
25
|
+
*
|
|
26
|
+
* ### `option`, not `menuitem`
|
|
27
|
+
*
|
|
28
|
+
* An `option` **is** something — the value being chosen — where a
|
|
29
|
+
* `menuitem` **does** something. So this carries `aria-selected`
|
|
30
|
+
* (which a menuitem must not), and a screen reader announces
|
|
31
|
+
* "listbox, selected, 2 of 4" rather than "menu, 4 items". It is a
|
|
32
|
+
* `<button>` with `role="option"` so it is natively focusable for the
|
|
33
|
+
* roving-tabindex move, the same shape `Menu` uses.
|
|
34
|
+
*/
|
|
35
|
+
export declare const ListboxOption: ({ isSelected, item, onSelect, registerFocus, registerSelection, tabIndex, trackElement, }: ListboxOptionProps) => ReactNode;
|
|
36
|
+
//# sourceMappingURL=ListboxOption.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ListboxOption.d.ts","sourceRoot":"","sources":["../../src/Listbox/ListboxOption.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAEhD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,UAAU,EAAE,OAAO,CAAA;IACnB,IAAI,EAAE,WAAW,CAAA;IACjB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,kEAAkE;IAClE,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,CAAA;IAC5C,0DAA0D;IAC1D,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,CAAA;IAChD,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,CACZ,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,iBAAiB,GAAG,IAAI,KAC9B,IAAI,CAAA;CACV,CAAA;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,aAAa,GAAI,2FAQ3B,kBAAkB,KAAG,SAoDvB,CAAA"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { FOCUS_RING_CLASS } from "../intentStyles.js";
|
|
4
|
+
import { toClassName } from "../toClassName.js";
|
|
5
|
+
/**
|
|
6
|
+
* Its own file for the same reason as `MenuAction` and `TabTrigger`:
|
|
7
|
+
* **both registrations are effects**, and an effect cannot run in a
|
|
8
|
+
* loop.
|
|
9
|
+
*
|
|
10
|
+
* The two registrations are separate on purpose, exactly as
|
|
11
|
+
* `TabTrigger` splits them. A disabled option joins the *choice* — it
|
|
12
|
+
* can still be the seeded `selectedValue` and still announces
|
|
13
|
+
* `aria-selected` — and stays out of the *focus* group, so the arrow
|
|
14
|
+
* keys and type-ahead skip it without `RovingFocus` having to know
|
|
15
|
+
* what "disabled" means.
|
|
16
|
+
*
|
|
17
|
+
* ### `option`, not `menuitem`
|
|
18
|
+
*
|
|
19
|
+
* An `option` **is** something — the value being chosen — where a
|
|
20
|
+
* `menuitem` **does** something. So this carries `aria-selected`
|
|
21
|
+
* (which a menuitem must not), and a screen reader announces
|
|
22
|
+
* "listbox, selected, 2 of 4" rather than "menu, 4 items". It is a
|
|
23
|
+
* `<button>` with `role="option"` so it is natively focusable for the
|
|
24
|
+
* roving-tabindex move, the same shape `Menu` uses.
|
|
25
|
+
*/
|
|
26
|
+
export const ListboxOption = ({ isSelected, item, onSelect, registerFocus, registerSelection, tabIndex, trackElement, }) => {
|
|
27
|
+
const { isDisabled = false, label, value } = item;
|
|
28
|
+
useEffect(() => registerSelection(value), [registerSelection, value]);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (isDisabled) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
return registerFocus(value);
|
|
34
|
+
}, [isDisabled, registerFocus, value]);
|
|
35
|
+
return (_jsxs("button", { "aria-selected": isSelected, className: toClassName("flex w-full cursor-pointer items-center justify-between gap-2 rounded-sm bg-transparent px-2 py-1.5 text-start text-content-primary text-sm transition-colors duration-(--duration-fast) ease-standard", "hover:bg-intent-neutral-surface", isSelected && "bg-intent-accent-surface", isDisabled &&
|
|
36
|
+
"cursor-not-allowed text-content-disabled hover:bg-transparent", FOCUS_RING_CLASS), disabled: isDisabled, onClick: () => {
|
|
37
|
+
onSelect(value);
|
|
38
|
+
}, ref: (element) => {
|
|
39
|
+
trackElement(value, element);
|
|
40
|
+
}, role: "option", tabIndex: tabIndex, type: "button", children: [label, isSelected ? (_jsx("span", { "aria-hidden": "true", className: "shrink-0 text-intent-accent-content", children: "\u2713" })) : null] }));
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=ListboxOption.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ListboxOption.js","sourceRoot":"","sources":["../../src/Listbox/ListboxOption.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAkB/C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,EAC5B,UAAU,EACV,IAAI,EACJ,QAAQ,EACR,aAAa,EACb,iBAAiB,EACjB,QAAQ,EACR,YAAY,GACO,EAAa,EAAE;IAClC,MAAM,EAAE,UAAU,GAAG,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,IAAI,CAAA;IAEjD,SAAS,CACP,GAAG,EAAE,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAC9B,CAAC,iBAAiB,EAAE,KAAK,CAAC,CAC3B,CAAA;IAED,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,UAAU,EAAE,CAAC;YACf,OAAM;QACR,CAAC;QAED,OAAO,aAAa,CAAC,KAAK,CAAC,CAAA;IAC7B,CAAC,EAAE,CAAC,UAAU,EAAE,aAAa,EAAE,KAAK,CAAC,CAAC,CAAA;IAEtC,OAAO,CACL,mCACiB,UAAU,EACzB,SAAS,EAAE,WAAW,CACpB,wMAAwM,EACxM,iCAAiC,EACjC,UAAU,IAAI,0BAA0B,EACxC,UAAU;YACR,+DAA+D,EACjE,gBAAgB,CACjB,EACD,QAAQ,EAAE,UAAU,EACpB,OAAO,EAAE,GAAG,EAAE;YACZ,QAAQ,CAAC,KAAK,CAAC,CAAA;QACjB,CAAC,EACD,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE;YACf,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;QAC9B,CAAC,EACD,IAAI,EAAC,QAAQ,EACb,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAC,QAAQ,aAEZ,KAAK,EAEL,UAAU,CAAC,CAAC,CAAC,CACZ,8BAGc,MAAM,EAClB,SAAS,EAAC,qCAAqC,uBAG1C,CACR,CAAC,CAAC,CAAC,IAAI,IACD,CACV,CAAA;AACH,CAAC,CAAA"}
|
package/dist/Menu/Menu.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Menu.d.ts","sourceRoot":"","sources":["../../src/Menu/Menu.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Menu.d.ts","sourceRoot":"","sources":["../../src/Menu/Menu.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAKnD,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAQpD,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,SAAS,CAAA;IAChB,QAAQ,EAAE,MAAM,IAAI,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,QAAQ,EAAE,CAAA;IACjB,iEAAiE;IACjE,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,+DAA+D;IAC/D,OAAO,EAAE,YAAY,CAAA;CACtB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,eAAO,MAAM,IAAI,GAAI,iEAOlB,SAAS,KAAG,SAuId,CAAA"}
|