@c9up/nebula 0.1.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/LICENSE +21 -0
- package/README.md +256 -0
- package/dist/adapters/css.d.ts +33 -0
- package/dist/adapters/css.js +67 -0
- package/dist/adapters/index.d.ts +16 -0
- package/dist/adapters/index.js +27 -0
- package/dist/adapters/tailwind.d.ts +25 -0
- package/dist/adapters/tailwind.js +122 -0
- package/dist/adapters/types.d.ts +49 -0
- package/dist/adapters/types.js +21 -0
- package/dist/adapters/unocss.d.ts +17 -0
- package/dist/adapters/unocss.js +107 -0
- package/dist/atoms/AspectRatio.d.ts +17 -0
- package/dist/atoms/AspectRatio.js +19 -0
- package/dist/atoms/Avatar.d.ts +23 -0
- package/dist/atoms/Avatar.js +46 -0
- package/dist/atoms/Badge.d.ts +25 -0
- package/dist/atoms/Badge.js +28 -0
- package/dist/atoms/Button.d.ts +58 -0
- package/dist/atoms/Button.js +67 -0
- package/dist/atoms/Checkbox.d.ts +41 -0
- package/dist/atoms/Checkbox.js +47 -0
- package/dist/atoms/Input.d.ts +36 -0
- package/dist/atoms/Input.js +50 -0
- package/dist/atoms/Kbd.d.ts +14 -0
- package/dist/atoms/Kbd.js +17 -0
- package/dist/atoms/Label.d.ts +24 -0
- package/dist/atoms/Label.js +26 -0
- package/dist/atoms/Marker.d.ts +42 -0
- package/dist/atoms/Marker.js +55 -0
- package/dist/atoms/NativeSelect.d.ts +43 -0
- package/dist/atoms/NativeSelect.js +81 -0
- package/dist/atoms/Progress.d.ts +21 -0
- package/dist/atoms/Progress.js +48 -0
- package/dist/atoms/ScrollArea.d.ts +28 -0
- package/dist/atoms/ScrollArea.js +37 -0
- package/dist/atoms/Separator.d.ts +16 -0
- package/dist/atoms/Separator.js +21 -0
- package/dist/atoms/Skeleton.d.ts +13 -0
- package/dist/atoms/Skeleton.js +18 -0
- package/dist/atoms/Slider.d.ts +30 -0
- package/dist/atoms/Slider.js +40 -0
- package/dist/atoms/Spinner.d.ts +17 -0
- package/dist/atoms/Spinner.js +20 -0
- package/dist/atoms/Switch.d.ts +24 -0
- package/dist/atoms/Switch.js +41 -0
- package/dist/atoms/Textarea.d.ts +28 -0
- package/dist/atoms/Textarea.js +36 -0
- package/dist/atoms/Toggle.d.ts +38 -0
- package/dist/atoms/Toggle.js +45 -0
- package/dist/atoms/index.d.ts +32 -0
- package/dist/atoms/index.js +32 -0
- package/dist/cli/add.d.ts +44 -0
- package/dist/cli/add.js +99 -0
- package/dist/cli/index.d.ts +10 -0
- package/dist/cli/index.js +10 -0
- package/dist/cli/registry.d.ts +31 -0
- package/dist/cli/registry.js +83 -0
- package/dist/config.d.ts +81 -0
- package/dist/config.js +43 -0
- package/dist/configure.d.ts +34 -0
- package/dist/configure.js +85 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +41 -0
- package/dist/lib/children.d.ts +28 -0
- package/dist/lib/children.js +27 -0
- package/dist/lib/cn.d.ts +13 -0
- package/dist/lib/cn.js +13 -0
- package/dist/lib/cva.d.ts +83 -0
- package/dist/lib/cva.js +178 -0
- package/dist/lib/icons.d.ts +44 -0
- package/dist/lib/icons.js +79 -0
- package/dist/lib/id.d.ts +31 -0
- package/dist/lib/id.js +41 -0
- package/dist/lib/index.d.ts +12 -0
- package/dist/lib/index.js +12 -0
- package/dist/lib/motion.d.ts +31 -0
- package/dist/lib/motion.js +39 -0
- package/dist/lib/props.d.ts +48 -0
- package/dist/lib/props.js +59 -0
- package/dist/lib/styled.d.ts +31 -0
- package/dist/lib/styled.js +39 -0
- package/dist/molecules/Accordion.d.ts +35 -0
- package/dist/molecules/Accordion.js +117 -0
- package/dist/molecules/Alert.d.ts +30 -0
- package/dist/molecules/Alert.js +35 -0
- package/dist/molecules/Attachment.d.ts +39 -0
- package/dist/molecules/Attachment.js +103 -0
- package/dist/molecules/Breadcrumb.d.ts +29 -0
- package/dist/molecules/Breadcrumb.js +73 -0
- package/dist/molecules/Bubble.d.ts +56 -0
- package/dist/molecules/Bubble.js +92 -0
- package/dist/molecules/ButtonGroup.d.ts +23 -0
- package/dist/molecules/ButtonGroup.js +30 -0
- package/dist/molecules/Card.d.ts +17 -0
- package/dist/molecules/Card.js +17 -0
- package/dist/molecules/Collapsible.d.ts +40 -0
- package/dist/molecules/Collapsible.js +71 -0
- package/dist/molecules/Empty.d.ts +16 -0
- package/dist/molecules/Empty.js +16 -0
- package/dist/molecules/Field.d.ts +54 -0
- package/dist/molecules/Field.js +82 -0
- package/dist/molecules/InputGroup.d.ts +33 -0
- package/dist/molecules/InputGroup.js +35 -0
- package/dist/molecules/InputOTP.d.ts +33 -0
- package/dist/molecules/InputOTP.js +140 -0
- package/dist/molecules/Item.d.ts +41 -0
- package/dist/molecules/Item.js +48 -0
- package/dist/molecules/Message.d.ts +43 -0
- package/dist/molecules/Message.js +82 -0
- package/dist/molecules/Pagination.d.ts +41 -0
- package/dist/molecules/Pagination.js +144 -0
- package/dist/molecules/RadioGroup.d.ts +32 -0
- package/dist/molecules/RadioGroup.js +65 -0
- package/dist/molecules/Resizable.d.ts +37 -0
- package/dist/molecules/Resizable.js +136 -0
- package/dist/molecules/Table.d.ts +37 -0
- package/dist/molecules/Table.js +78 -0
- package/dist/molecules/Tabs.d.ts +37 -0
- package/dist/molecules/Tabs.js +109 -0
- package/dist/molecules/ToggleGroup.d.ts +37 -0
- package/dist/molecules/ToggleGroup.js +68 -0
- package/dist/molecules/Typography.d.ts +26 -0
- package/dist/molecules/Typography.js +67 -0
- package/dist/molecules/index.d.ts +37 -0
- package/dist/molecules/index.js +37 -0
- package/dist/organisms/AlertDialog.d.ts +40 -0
- package/dist/organisms/AlertDialog.js +104 -0
- package/dist/organisms/Calendar.d.ts +96 -0
- package/dist/organisms/Calendar.js +353 -0
- package/dist/organisms/Carousel.d.ts +29 -0
- package/dist/organisms/Carousel.js +128 -0
- package/dist/organisms/Chart.d.ts +55 -0
- package/dist/organisms/Chart.js +189 -0
- package/dist/organisms/Combobox.d.ts +41 -0
- package/dist/organisms/Combobox.js +102 -0
- package/dist/organisms/Command.d.ts +46 -0
- package/dist/organisms/Command.js +171 -0
- package/dist/organisms/CommandDialog.d.ts +42 -0
- package/dist/organisms/CommandDialog.js +116 -0
- package/dist/organisms/ContextMenu.d.ts +29 -0
- package/dist/organisms/ContextMenu.js +102 -0
- package/dist/organisms/DataTable.d.ts +50 -0
- package/dist/organisms/DataTable.js +257 -0
- package/dist/organisms/DatePicker.d.ts +34 -0
- package/dist/organisms/DatePicker.js +115 -0
- package/dist/organisms/DateRangePicker.d.ts +34 -0
- package/dist/organisms/DateRangePicker.js +122 -0
- package/dist/organisms/Dialog.d.ts +50 -0
- package/dist/organisms/Dialog.js +116 -0
- package/dist/organisms/Drawer.d.ts +40 -0
- package/dist/organisms/Drawer.js +131 -0
- package/dist/organisms/DropdownMenu.d.ts +30 -0
- package/dist/organisms/DropdownMenu.js +91 -0
- package/dist/organisms/Form.d.ts +114 -0
- package/dist/organisms/Form.js +151 -0
- package/dist/organisms/HoverCard.d.ts +30 -0
- package/dist/organisms/HoverCard.js +68 -0
- package/dist/organisms/Menubar.d.ts +28 -0
- package/dist/organisms/Menubar.js +147 -0
- package/dist/organisms/MessageScroller.d.ts +36 -0
- package/dist/organisms/MessageScroller.js +124 -0
- package/dist/organisms/NavigationMenu.d.ts +41 -0
- package/dist/organisms/NavigationMenu.js +139 -0
- package/dist/organisms/Popover.d.ts +33 -0
- package/dist/organisms/Popover.js +58 -0
- package/dist/organisms/Questionnaire.d.ts +74 -0
- package/dist/organisms/Questionnaire.js +235 -0
- package/dist/organisms/Select.d.ts +47 -0
- package/dist/organisms/Select.js +250 -0
- package/dist/organisms/Sheet.d.ts +34 -0
- package/dist/organisms/Sheet.js +107 -0
- package/dist/organisms/Sidebar.d.ts +100 -0
- package/dist/organisms/Sidebar.js +228 -0
- package/dist/organisms/Toaster.d.ts +67 -0
- package/dist/organisms/Toaster.js +189 -0
- package/dist/organisms/Tooltip.d.ts +39 -0
- package/dist/organisms/Tooltip.js +94 -0
- package/dist/organisms/index.d.ts +40 -0
- package/dist/organisms/index.js +40 -0
- package/dist/primitives/controllable.d.ts +38 -0
- package/dist/primitives/controllable.js +43 -0
- package/dist/primitives/dismissable.d.ts +53 -0
- package/dist/primitives/dismissable.js +158 -0
- package/dist/primitives/floating.d.ts +116 -0
- package/dist/primitives/floating.js +289 -0
- package/dist/primitives/floatingSurface.d.ts +56 -0
- package/dist/primitives/floatingSurface.js +142 -0
- package/dist/primitives/focusTrap.d.ts +47 -0
- package/dist/primitives/focusTrap.js +101 -0
- package/dist/primitives/focusable.d.ts +53 -0
- package/dist/primitives/focusable.js +109 -0
- package/dist/primitives/index.d.ts +27 -0
- package/dist/primitives/index.js +27 -0
- package/dist/primitives/menu.d.ts +102 -0
- package/dist/primitives/menu.js +356 -0
- package/dist/primitives/modalSurface.d.ts +54 -0
- package/dist/primitives/modalSurface.js +146 -0
- package/dist/primitives/portal.d.ts +34 -0
- package/dist/primitives/portal.js +42 -0
- package/dist/primitives/presence.d.ts +45 -0
- package/dist/primitives/presence.js +152 -0
- package/dist/primitives/rovingFocus.d.ts +47 -0
- package/dist/primitives/rovingFocus.js +176 -0
- package/dist/primitives/scrollLock.d.ts +21 -0
- package/dist/primitives/scrollLock.js +75 -0
- package/dist/primitives/typeahead.d.ts +39 -0
- package/dist/primitives/typeahead.js +123 -0
- package/dist/templates/AppShell.d.ts +30 -0
- package/dist/templates/AppShell.js +42 -0
- package/dist/templates/AuthLayout.d.ts +23 -0
- package/dist/templates/AuthLayout.js +42 -0
- package/dist/templates/SettingsLayout.d.ts +27 -0
- package/dist/templates/SettingsLayout.js +42 -0
- package/dist/templates/index.d.ts +16 -0
- package/dist/templates/index.js +16 -0
- package/nebula.css +2 -0
- package/package.json +92 -0
- package/registry.json +1107 -0
- package/src/adapters/css.ts +75 -0
- package/src/adapters/index.ts +40 -0
- package/src/adapters/tailwind.ts +134 -0
- package/src/adapters/types.ts +53 -0
- package/src/adapters/unocss.ts +116 -0
- package/src/atoms/AspectRatio.ts +28 -0
- package/src/atoms/Avatar.ts +67 -0
- package/src/atoms/Badge.ts +46 -0
- package/src/atoms/Button.ts +96 -0
- package/src/atoms/Checkbox.ts +76 -0
- package/src/atoms/Input.ts +76 -0
- package/src/atoms/Kbd.ts +28 -0
- package/src/atoms/Label.ts +38 -0
- package/src/atoms/Marker.ts +79 -0
- package/src/atoms/NativeSelect.ts +115 -0
- package/src/atoms/Progress.ts +64 -0
- package/src/atoms/ScrollArea.ts +53 -0
- package/src/atoms/Separator.ts +36 -0
- package/src/atoms/Skeleton.ts +24 -0
- package/src/atoms/Slider.ts +65 -0
- package/src/atoms/Spinner.ts +28 -0
- package/src/atoms/Switch.ts +56 -0
- package/src/atoms/Textarea.ts +58 -0
- package/src/atoms/Toggle.ts +67 -0
- package/src/atoms/index.ts +62 -0
- package/src/cli/add.ts +126 -0
- package/src/cli/index.ts +22 -0
- package/src/cli/registry.ts +116 -0
- package/src/config.ts +102 -0
- package/src/configure.ts +127 -0
- package/src/index.ts +53 -0
- package/src/lib/children.ts +43 -0
- package/src/lib/cn.ts +14 -0
- package/src/lib/cva.ts +249 -0
- package/src/lib/icons.ts +138 -0
- package/src/lib/id.ts +44 -0
- package/src/lib/index.ts +21 -0
- package/src/lib/motion.ts +48 -0
- package/src/lib/props.ts +83 -0
- package/src/lib/styled.ts +60 -0
- package/src/molecules/Accordion.ts +161 -0
- package/src/molecules/Alert.ts +58 -0
- package/src/molecules/Attachment.ts +132 -0
- package/src/molecules/Breadcrumb.ts +107 -0
- package/src/molecules/Bubble.ts +134 -0
- package/src/molecules/ButtonGroup.ts +55 -0
- package/src/molecules/Card.ts +42 -0
- package/src/molecules/Collapsible.ts +89 -0
- package/src/molecules/Empty.ts +43 -0
- package/src/molecules/Field.ts +131 -0
- package/src/molecules/InputGroup.ts +62 -0
- package/src/molecules/InputOTP.ts +166 -0
- package/src/molecules/Item.ts +89 -0
- package/src/molecules/Message.ts +127 -0
- package/src/molecules/Pagination.ts +197 -0
- package/src/molecules/RadioGroup.ts +104 -0
- package/src/molecules/Resizable.ts +166 -0
- package/src/molecules/Table.ts +118 -0
- package/src/molecules/Tabs.ts +148 -0
- package/src/molecules/ToggleGroup.ts +109 -0
- package/src/molecules/Typography.ts +94 -0
- package/src/molecules/index.ts +144 -0
- package/src/organisms/AlertDialog.ts +133 -0
- package/src/organisms/Calendar.ts +470 -0
- package/src/organisms/Carousel.ts +161 -0
- package/src/organisms/Chart.ts +257 -0
- package/src/organisms/Combobox.ts +140 -0
- package/src/organisms/Command.ts +211 -0
- package/src/organisms/CommandDialog.ts +149 -0
- package/src/organisms/ContextMenu.ts +125 -0
- package/src/organisms/DataTable.ts +328 -0
- package/src/organisms/DatePicker.ts +141 -0
- package/src/organisms/DateRangePicker.ts +149 -0
- package/src/organisms/Dialog.ts +162 -0
- package/src/organisms/Drawer.ts +167 -0
- package/src/organisms/DropdownMenu.ts +112 -0
- package/src/organisms/Form.ts +217 -0
- package/src/organisms/HoverCard.ts +92 -0
- package/src/organisms/Menubar.ts +167 -0
- package/src/organisms/MessageScroller.ts +145 -0
- package/src/organisms/NavigationMenu.ts +184 -0
- package/src/organisms/Popover.ts +84 -0
- package/src/organisms/Questionnaire.ts +322 -0
- package/src/organisms/Select.ts +286 -0
- package/src/organisms/Sheet.ts +144 -0
- package/src/organisms/Sidebar.ts +336 -0
- package/src/organisms/Toaster.ts +246 -0
- package/src/organisms/Tooltip.ts +124 -0
- package/src/organisms/index.ts +134 -0
- package/src/primitives/controllable.ts +64 -0
- package/src/primitives/dismissable.ts +195 -0
- package/src/primitives/floating.ts +453 -0
- package/src/primitives/floatingSurface.ts +209 -0
- package/src/primitives/focusTrap.ts +132 -0
- package/src/primitives/focusable.ts +108 -0
- package/src/primitives/index.ts +88 -0
- package/src/primitives/menu.ts +491 -0
- package/src/primitives/modalSurface.ts +199 -0
- package/src/primitives/portal.ts +61 -0
- package/src/primitives/presence.ts +185 -0
- package/src/primitives/rovingFocus.ts +212 -0
- package/src/primitives/scrollLock.ts +79 -0
- package/src/primitives/typeahead.ts +156 -0
- package/src/templates/AppShell.ts +56 -0
- package/src/templates/AuthLayout.ts +66 -0
- package/src/templates/SettingsLayout.ts +63 -0
- package/src/templates/index.ts +21 -0
- package/theme.css +151 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Floating surface — the whole lifecycle of a popover-shaped overlay.
|
|
4
|
+
*
|
|
5
|
+
* Popover, Tooltip, HoverCard, DropdownMenu, ContextMenu, Menubar, Select and
|
|
6
|
+
* Combobox are the same machine with different contents and different opening
|
|
7
|
+
* gestures. That machine is: portal out of the tree, position against an
|
|
8
|
+
* anchor, register as a dismissable layer, optionally trap focus, and on close
|
|
9
|
+
* let the exit animation finish before removing anything.
|
|
10
|
+
*
|
|
11
|
+
* Five primitives in a fixed order, and the order is the part that is easy to
|
|
12
|
+
* get wrong. The layer must register *after* the content exists, or its
|
|
13
|
+
* containment check has nothing to test against and the opening click closes
|
|
14
|
+
* it again. The focus trap must run after the first position pass, or it
|
|
15
|
+
* focuses a surface still sitting at (0,0) and the browser scrolls to it.
|
|
16
|
+
*
|
|
17
|
+
* Written once here so eight components cannot each get that order slightly
|
|
18
|
+
* different.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { effect, onMount, type TemplateResult } from "@c9up/aurora";
|
|
22
|
+
import {
|
|
23
|
+
type DismissableLayer,
|
|
24
|
+
type DismissReason,
|
|
25
|
+
dismissable,
|
|
26
|
+
} from "./dismissable.js";
|
|
27
|
+
import { type AutoPosition, autoPosition, type Placement } from "./floating.js";
|
|
28
|
+
import { firstFocusable, focusSilently } from "./focusable.js";
|
|
29
|
+
import { type FocusTrap, focusTrap } from "./focusTrap.js";
|
|
30
|
+
import { type Portal, portal } from "./portal.js";
|
|
31
|
+
import { onExitFinished } from "./presence.js";
|
|
32
|
+
|
|
33
|
+
export interface FloatingSurfaceOptions {
|
|
34
|
+
/** What the surface is positioned against — usually the trigger. */
|
|
35
|
+
anchor: () => HTMLElement | null;
|
|
36
|
+
/** Should the surface be showing? Read reactively. */
|
|
37
|
+
open: () => boolean;
|
|
38
|
+
/** The surface wants to close. Flip whatever drives `open`. */
|
|
39
|
+
onClose: (reason: DismissReason) => void;
|
|
40
|
+
/** The surface's markup. Built fresh on each open. */
|
|
41
|
+
content: () => TemplateResult;
|
|
42
|
+
|
|
43
|
+
placement?: Placement;
|
|
44
|
+
offset?: number;
|
|
45
|
+
/** Size the surface to the anchor. Select and Combobox want this. */
|
|
46
|
+
matchWidth?: boolean;
|
|
47
|
+
|
|
48
|
+
/** Keep keyboard focus inside while open. */
|
|
49
|
+
trapFocus?: boolean;
|
|
50
|
+
/** Move focus into the surface on open. Implied by `trapFocus`. */
|
|
51
|
+
autoFocus?: boolean;
|
|
52
|
+
/** Where focus goes first. Defaults to the first focusable element. */
|
|
53
|
+
initialFocus?: (content: HTMLElement) => HTMLElement | null;
|
|
54
|
+
|
|
55
|
+
escapeKey?: boolean;
|
|
56
|
+
outsidePointer?: boolean;
|
|
57
|
+
/** Default `false` — a surface that opened on hover should not steal focus. */
|
|
58
|
+
outsideFocus?: boolean;
|
|
59
|
+
|
|
60
|
+
/** The surface is live and positioned. Wire item handlers here. */
|
|
61
|
+
onOpened?: (content: HTMLElement) => void;
|
|
62
|
+
/** The surface is about to be removed. */
|
|
63
|
+
onClosed?: () => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface Live {
|
|
67
|
+
readonly mount: Portal;
|
|
68
|
+
/**
|
|
69
|
+
* The anchor this surface actually opened against.
|
|
70
|
+
*
|
|
71
|
+
* Held rather than re-read on close, because the accessor is often derived
|
|
72
|
+
* from the open state — Menubar and NavigationMenu resolve theirs from the
|
|
73
|
+
* index of the open menu, which is already cleared by the time the surface
|
|
74
|
+
* tears down. Asking again at that point returns null and the focus
|
|
75
|
+
* restoration silently does nothing.
|
|
76
|
+
*/
|
|
77
|
+
readonly anchor: HTMLElement;
|
|
78
|
+
readonly element: HTMLElement;
|
|
79
|
+
readonly position: AutoPosition;
|
|
80
|
+
readonly layer: DismissableLayer;
|
|
81
|
+
readonly trap: FocusTrap | null;
|
|
82
|
+
cancelExit: (() => void) | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Wire a floating surface into the surrounding component.
|
|
87
|
+
*
|
|
88
|
+
* Call from a component setup. It registers its own mount and unmount hooks,
|
|
89
|
+
* so there is nothing to dispose by hand.
|
|
90
|
+
*/
|
|
91
|
+
export function floatingSurface(options: FloatingSurfaceOptions): void {
|
|
92
|
+
let live: Live | null = null;
|
|
93
|
+
|
|
94
|
+
function show(): void {
|
|
95
|
+
if (live !== null) {
|
|
96
|
+
// Already open and mid-exit: cancel the teardown and reuse the node
|
|
97
|
+
// rather than stacking a second copy on top of the one fading out.
|
|
98
|
+
live.cancelExit?.();
|
|
99
|
+
live.cancelExit = null;
|
|
100
|
+
live.element.setAttribute("data-state", "open");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const anchor = options.anchor();
|
|
105
|
+
if (anchor === null) return;
|
|
106
|
+
|
|
107
|
+
const mount = portal(options.content());
|
|
108
|
+
const element = mount.host.firstElementChild;
|
|
109
|
+
if (!(element instanceof HTMLElement)) {
|
|
110
|
+
mount.close();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
element.setAttribute("data-state", "open");
|
|
115
|
+
|
|
116
|
+
const position = autoPosition(anchor, element, {
|
|
117
|
+
placement: options.placement,
|
|
118
|
+
offset: options.offset,
|
|
119
|
+
matchWidth: options.matchWidth,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const layer = dismissable({
|
|
123
|
+
element: () => element,
|
|
124
|
+
exclude: () => [anchor],
|
|
125
|
+
onDismiss: options.onClose,
|
|
126
|
+
escapeKey: options.escapeKey,
|
|
127
|
+
outsidePointer: options.outsidePointer,
|
|
128
|
+
outsideFocus: options.outsideFocus ?? false,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
let trap: FocusTrap | null = null;
|
|
132
|
+
if (options.trapFocus === true) {
|
|
133
|
+
trap = focusTrap(element, {
|
|
134
|
+
initialFocus: () => options.initialFocus?.(element) ?? null,
|
|
135
|
+
});
|
|
136
|
+
} else if (options.autoFocus === true) {
|
|
137
|
+
focusSilently(options.initialFocus?.(element) ?? firstFocusable(element));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
live = { mount, anchor, element, position, layer, trap, cancelExit: null };
|
|
141
|
+
options.onOpened?.(element);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function hide(): void {
|
|
145
|
+
const current = live;
|
|
146
|
+
if (current === null) return;
|
|
147
|
+
|
|
148
|
+
// Detach behaviour first, then animate out. A surface that is fading
|
|
149
|
+
// should not still be answering Escape or repositioning itself.
|
|
150
|
+
current.layer.remove();
|
|
151
|
+
current.position.stop();
|
|
152
|
+
current.trap?.release();
|
|
153
|
+
returnFocusToAnchor(current.element, current.anchor);
|
|
154
|
+
current.element.setAttribute("data-state", "closed");
|
|
155
|
+
options.onClosed?.();
|
|
156
|
+
|
|
157
|
+
current.cancelExit = onExitFinished(current.element, () => {
|
|
158
|
+
current.mount.close();
|
|
159
|
+
if (live === current) live = null;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Hand focus back to the anchor when the surface held it.
|
|
165
|
+
*
|
|
166
|
+
* Dismissal by keyboard leaves focus inside a subtree that is about to be
|
|
167
|
+
* removed; without this it falls to `<body>` and the next Tab restarts at
|
|
168
|
+
* the top of the page. `dismissable` handles Escape from a *captured*
|
|
169
|
+
* document listener and stops propagation, so a component cannot do this in
|
|
170
|
+
* its own keydown handler — the event never reaches it.
|
|
171
|
+
*
|
|
172
|
+
* Guarded on containment, so a dismissal caused by clicking somewhere else
|
|
173
|
+
* does not steal focus back from wherever the user just put it. A trapped
|
|
174
|
+
* surface has already restored focus by the time this runs, and the guard
|
|
175
|
+
* makes it a no-op there too.
|
|
176
|
+
*/
|
|
177
|
+
function returnFocusToAnchor(
|
|
178
|
+
surface: HTMLElement,
|
|
179
|
+
anchor: HTMLElement,
|
|
180
|
+
): void {
|
|
181
|
+
const active = document.activeElement;
|
|
182
|
+
if (!(active instanceof Node) || !surface.contains(active)) return;
|
|
183
|
+
focusSilently(anchor);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function teardown(): void {
|
|
187
|
+
const current = live;
|
|
188
|
+
if (current === null) return;
|
|
189
|
+
live = null;
|
|
190
|
+
current.cancelExit?.();
|
|
191
|
+
current.layer.remove();
|
|
192
|
+
current.position.stop();
|
|
193
|
+
current.trap?.release();
|
|
194
|
+
current.mount.close();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
onMount(() => {
|
|
198
|
+
const stop = effect(() => {
|
|
199
|
+
if (options.open()) show();
|
|
200
|
+
else hide();
|
|
201
|
+
});
|
|
202
|
+
// The component is going away, so the exit animation has nobody left to
|
|
203
|
+
// play for — drop the node immediately rather than orphaning it in body.
|
|
204
|
+
return () => {
|
|
205
|
+
stop();
|
|
206
|
+
teardown();
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Focus trap — keyboard focus cannot leave a subtree while it is active.
|
|
4
|
+
*
|
|
5
|
+
* Required by every modal surface (Dialog, AlertDialog, Sheet, Drawer). The
|
|
6
|
+
* WAI-ARIA dialog pattern is specific about it: Tab from the last focusable
|
|
7
|
+
* element wraps to the first, Shift+Tab from the first wraps to the last, and
|
|
8
|
+
* focus returns to whatever opened the dialog when it closes.
|
|
9
|
+
*
|
|
10
|
+
* Two mechanisms, deliberately both:
|
|
11
|
+
*
|
|
12
|
+
* - A `keydown` handler implements the wrap. This is the path that runs for
|
|
13
|
+
* ordinary keyboard use, and it is the only one that can put focus on the
|
|
14
|
+
* *correct* end of the trap.
|
|
15
|
+
* - A `focusin` handler on the document catches everything else — a click on
|
|
16
|
+
* the page behind, a screen reader jumping by landmark, a browser find bar
|
|
17
|
+
* handing focus back somewhere else. It pulls focus back into the trap.
|
|
18
|
+
*
|
|
19
|
+
* The keydown handler alone leaks; the focusin handler alone produces the
|
|
20
|
+
* classic bug where Tab escapes and is yanked back a frame later, which reads
|
|
21
|
+
* as a flicker and confuses assistive technology.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { firstFocusable, focusableWithin, focusSilently } from "./focusable.js";
|
|
25
|
+
|
|
26
|
+
export interface FocusTrapOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Where focus goes when the trap activates. Defaults to the first focusable
|
|
29
|
+
* descendant, then the container itself.
|
|
30
|
+
*/
|
|
31
|
+
initialFocus?: () => HTMLElement | null;
|
|
32
|
+
/**
|
|
33
|
+
* Where focus goes when it releases. Defaults to whatever was focused when
|
|
34
|
+
* the trap activated — which is nearly always the trigger.
|
|
35
|
+
*/
|
|
36
|
+
returnFocus?: () => HTMLElement | null;
|
|
37
|
+
/** Skip moving focus on activation. For surfaces that focus themselves. */
|
|
38
|
+
skipInitialFocus?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface FocusTrap {
|
|
42
|
+
/** Tear the trap down and restore focus. Safe to call twice. */
|
|
43
|
+
release(): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Trap focus inside `container` until the returned handle is released.
|
|
48
|
+
*
|
|
49
|
+
* The container is made focusable with `tabindex="-1"` if it is not already,
|
|
50
|
+
* so an empty surface still has somewhere to put focus. Without it, focus
|
|
51
|
+
* would fall to `<body>` and the next Tab would land outside the trap.
|
|
52
|
+
*/
|
|
53
|
+
export function focusTrap(
|
|
54
|
+
container: HTMLElement,
|
|
55
|
+
options: FocusTrapOptions = {},
|
|
56
|
+
): FocusTrap {
|
|
57
|
+
const previouslyFocused = activeElement();
|
|
58
|
+
|
|
59
|
+
if (!container.hasAttribute("tabindex")) {
|
|
60
|
+
container.setAttribute("tabindex", "-1");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!options.skipInitialFocus) {
|
|
64
|
+
const target =
|
|
65
|
+
options.initialFocus?.() ?? firstFocusable(container) ?? container;
|
|
66
|
+
focusSilently(target);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function onKeyDown(event: KeyboardEvent): void {
|
|
70
|
+
if (event.key !== "Tab") return;
|
|
71
|
+
|
|
72
|
+
const focusables = focusableWithin(container);
|
|
73
|
+
if (focusables.length === 0) {
|
|
74
|
+
// Nothing to cycle through: hold focus on the container so Tab cannot
|
|
75
|
+
// walk out into the page behind.
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
focusSilently(container);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const first = focusables[0];
|
|
82
|
+
const last = focusables[focusables.length - 1];
|
|
83
|
+
const current = activeElement();
|
|
84
|
+
|
|
85
|
+
if (event.shiftKey && (current === first || current === container)) {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
focusSilently(last);
|
|
88
|
+
} else if (!event.shiftKey && current === last) {
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
focusSilently(first);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Pull focus back when it lands outside by any route other than Tab.
|
|
96
|
+
*
|
|
97
|
+
* Guarded on `contains`, and only ever moves focus *into* the trap, so it
|
|
98
|
+
* cannot fight a nested trap: an inner dialog's container is a descendant
|
|
99
|
+
* of the outer one, so the outer handler sees the focus as already inside.
|
|
100
|
+
*/
|
|
101
|
+
function onFocusIn(event: FocusEvent): void {
|
|
102
|
+
const target = event.target;
|
|
103
|
+
if (!(target instanceof Node)) return;
|
|
104
|
+
if (container.contains(target)) return;
|
|
105
|
+
focusSilently(firstFocusable(container) ?? container);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
109
|
+
document.addEventListener("focusin", onFocusIn, true);
|
|
110
|
+
|
|
111
|
+
let released = false;
|
|
112
|
+
return {
|
|
113
|
+
release(): void {
|
|
114
|
+
if (released) return;
|
|
115
|
+
released = true;
|
|
116
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
117
|
+
document.removeEventListener("focusin", onFocusIn, true);
|
|
118
|
+
|
|
119
|
+
const target = options.returnFocus?.() ?? previouslyFocused;
|
|
120
|
+
// Only restore focus if it is still inside the trap. If something else
|
|
121
|
+
// has already claimed it — a second dialog, a toast action — stealing it
|
|
122
|
+
// back would be the wrong call.
|
|
123
|
+
if (container.contains(activeElement())) focusSilently(target);
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The focused element, narrowed to something we can call `.focus()` on. */
|
|
129
|
+
function activeElement(): HTMLElement | null {
|
|
130
|
+
const active = document.activeElement;
|
|
131
|
+
return active instanceof HTMLElement ? active : null;
|
|
132
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Which elements can take focus.
|
|
4
|
+
*
|
|
5
|
+
* The focus trap, the roving-focus groups and every overlay that restores
|
|
6
|
+
* focus on close all need the same answer, and getting it slightly different
|
|
7
|
+
* in each place is how keyboard navigation rots. One implementation here.
|
|
8
|
+
*
|
|
9
|
+
* The selector is the standard tabbable set, but a selector alone is not the
|
|
10
|
+
* answer: it matches nodes that are disabled, `inert`, hidden by CSS, or
|
|
11
|
+
* inside a collapsed `<details>`. Those have to be filtered by asking the
|
|
12
|
+
* layout engine, which is why `isVisible` reads `offsetParent` and computed
|
|
13
|
+
* styles rather than trusting the markup.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const FOCUSABLE_SELECTOR = [
|
|
17
|
+
"a[href]",
|
|
18
|
+
"area[href]",
|
|
19
|
+
"button:not([disabled])",
|
|
20
|
+
"input:not([disabled]):not([type='hidden'])",
|
|
21
|
+
"select:not([disabled])",
|
|
22
|
+
"textarea:not([disabled])",
|
|
23
|
+
"iframe",
|
|
24
|
+
"object",
|
|
25
|
+
"embed",
|
|
26
|
+
"audio[controls]",
|
|
27
|
+
"video[controls]",
|
|
28
|
+
"summary",
|
|
29
|
+
"[contenteditable]:not([contenteditable='false'])",
|
|
30
|
+
"[tabindex]",
|
|
31
|
+
].join(",");
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Is the element rendered and interactive right now?
|
|
35
|
+
*
|
|
36
|
+
* `offsetParent === null` catches `display: none` on the element or any
|
|
37
|
+
* ancestor in one property read, which is much cheaper than walking the tree.
|
|
38
|
+
* It reports null for `position: fixed` elements too, so those fall through to
|
|
39
|
+
* the explicit style checks — that branch is why dialogs and popovers, which
|
|
40
|
+
* are routinely fixed, are not wrongly treated as hidden.
|
|
41
|
+
*/
|
|
42
|
+
export function isVisible(el: HTMLElement): boolean {
|
|
43
|
+
if (el.hasAttribute("inert")) return false;
|
|
44
|
+
if (el.closest("[inert]") !== null) return false;
|
|
45
|
+
|
|
46
|
+
if (el.offsetParent !== null) return true;
|
|
47
|
+
|
|
48
|
+
const style = getComputedStyle(el);
|
|
49
|
+
if (style.position !== "fixed") return false;
|
|
50
|
+
return style.display !== "none" && style.visibility !== "hidden";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Can this element receive focus via the keyboard?
|
|
55
|
+
*
|
|
56
|
+
* Focusability is decided from the selector match plus the `tabindex`
|
|
57
|
+
* *attribute* — deliberately not the `tabIndex` property. The property is
|
|
58
|
+
* derived: a browser reports `0` for a native `<button>` that never declared
|
|
59
|
+
* one, and DOM implementations disagree about which elements get that
|
|
60
|
+
* treatment. Reading the attribute asks what the markup actually says, and
|
|
61
|
+
* "natively focusable unless it opted out" is then a rule this function states
|
|
62
|
+
* rather than one it inherits from whichever DOM it is running in.
|
|
63
|
+
*/
|
|
64
|
+
export function isFocusable(el: Element): el is HTMLElement {
|
|
65
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
66
|
+
if (!el.matches(FOCUSABLE_SELECTOR)) return false;
|
|
67
|
+
if (el.hasAttribute("disabled")) return false;
|
|
68
|
+
if (el.getAttribute("aria-hidden") === "true") return false;
|
|
69
|
+
|
|
70
|
+
const declared = el.getAttribute("tabindex");
|
|
71
|
+
if (declared !== null && Number.parseInt(declared, 10) < 0) return false;
|
|
72
|
+
|
|
73
|
+
return isVisible(el);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Every focusable descendant of `root`, in tab order.
|
|
78
|
+
*
|
|
79
|
+
* Document order is returned as-is. Honouring positive `tabindex` values would
|
|
80
|
+
* mean a second sort, and nebula never emits one — a positive tabindex breaks
|
|
81
|
+
* the tab order of the whole page, so the library treats its presence in host
|
|
82
|
+
* markup as the host's problem rather than reordering around it.
|
|
83
|
+
*/
|
|
84
|
+
export function focusableWithin(root: ParentNode): HTMLElement[] {
|
|
85
|
+
const found: HTMLElement[] = [];
|
|
86
|
+
for (const candidate of root.querySelectorAll(FOCUSABLE_SELECTOR)) {
|
|
87
|
+
if (isFocusable(candidate)) found.push(candidate);
|
|
88
|
+
}
|
|
89
|
+
return found;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** First focusable descendant, or `null` when the subtree has none. */
|
|
93
|
+
export function firstFocusable(root: ParentNode): HTMLElement | null {
|
|
94
|
+
for (const candidate of root.querySelectorAll(FOCUSABLE_SELECTOR)) {
|
|
95
|
+
if (isFocusable(candidate)) return candidate;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Focus an element without scrolling the page to it.
|
|
102
|
+
*
|
|
103
|
+
* Overlays position themselves; letting the browser scroll to the newly
|
|
104
|
+
* focused node fights that positioning and visibly jumps the page.
|
|
105
|
+
*/
|
|
106
|
+
export function focusSilently(el: HTMLElement | null | undefined): void {
|
|
107
|
+
el?.focus({ preventScroll: true });
|
|
108
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless primitives — the behaviour layer nebula owns instead of Radix.
|
|
3
|
+
*
|
|
4
|
+
* shadcn/ui is styling on top of Radix. Radix is React-only, so porting the
|
|
5
|
+
* styling alone would leave every interactive component inert. These modules
|
|
6
|
+
* are that missing half, written against Aurora's signals and DOM: focus
|
|
7
|
+
* management, layer dismissal, placement, presence, roving focus, type-ahead
|
|
8
|
+
* and controlled/uncontrolled state.
|
|
9
|
+
*
|
|
10
|
+
* They are deliberately usable on their own. A component the registry does not
|
|
11
|
+
* ship — an app's own combobox, a bespoke overlay — should be able to reach
|
|
12
|
+
* for the same trap and the same layer stack rather than growing a second,
|
|
13
|
+
* subtly different implementation next door.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
type Controllable,
|
|
18
|
+
type ControllableOptions,
|
|
19
|
+
controllable,
|
|
20
|
+
} from "./controllable.js";
|
|
21
|
+
export {
|
|
22
|
+
type DismissableLayer,
|
|
23
|
+
type DismissableOptions,
|
|
24
|
+
type DismissReason,
|
|
25
|
+
dismissable,
|
|
26
|
+
layerCount,
|
|
27
|
+
} from "./dismissable.js";
|
|
28
|
+
export {
|
|
29
|
+
type Align,
|
|
30
|
+
type AutoPosition,
|
|
31
|
+
type AutoPositionOptions,
|
|
32
|
+
autoPosition,
|
|
33
|
+
type Placement,
|
|
34
|
+
type Position,
|
|
35
|
+
type PositionOptions,
|
|
36
|
+
type Rect,
|
|
37
|
+
resolvePosition,
|
|
38
|
+
type Side,
|
|
39
|
+
type Viewport,
|
|
40
|
+
} from "./floating.js";
|
|
41
|
+
export {
|
|
42
|
+
type FloatingSurfaceOptions,
|
|
43
|
+
floatingSurface,
|
|
44
|
+
} from "./floatingSurface.js";
|
|
45
|
+
export {
|
|
46
|
+
firstFocusable,
|
|
47
|
+
focusableWithin,
|
|
48
|
+
focusSilently,
|
|
49
|
+
isFocusable,
|
|
50
|
+
isVisible,
|
|
51
|
+
} from "./focusable.js";
|
|
52
|
+
export {
|
|
53
|
+
type FocusTrap,
|
|
54
|
+
type FocusTrapOptions,
|
|
55
|
+
focusTrap,
|
|
56
|
+
} from "./focusTrap.js";
|
|
57
|
+
export {
|
|
58
|
+
type MenuAction,
|
|
59
|
+
type MenuCheckbox,
|
|
60
|
+
type MenuEntry,
|
|
61
|
+
type MenuLabel,
|
|
62
|
+
type MenuRadioGroup,
|
|
63
|
+
type MenuSeparator,
|
|
64
|
+
type MenuSubmenu,
|
|
65
|
+
menuPanel,
|
|
66
|
+
menuPanelClasses,
|
|
67
|
+
wireMenu,
|
|
68
|
+
} from "./menu.js";
|
|
69
|
+
export { type ModalSurfaceOptions, modalSurface } from "./modalSurface.js";
|
|
70
|
+
export { type Portal, type PortalOptions, portal } from "./portal.js";
|
|
71
|
+
export {
|
|
72
|
+
onExitFinished,
|
|
73
|
+
type Presence,
|
|
74
|
+
type PresenceState,
|
|
75
|
+
presence,
|
|
76
|
+
} from "./presence.js";
|
|
77
|
+
export {
|
|
78
|
+
type Orientation,
|
|
79
|
+
type RovingFocus,
|
|
80
|
+
type RovingFocusOptions,
|
|
81
|
+
rovingFocus,
|
|
82
|
+
} from "./rovingFocus.js";
|
|
83
|
+
export { isScrollLocked, lockScroll } from "./scrollLock.js";
|
|
84
|
+
export {
|
|
85
|
+
type Typeahead,
|
|
86
|
+
type TypeaheadOptions,
|
|
87
|
+
typeahead,
|
|
88
|
+
} from "./typeahead.js";
|