rift-ds 1.13.0 → 1.15.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/README.md +14 -6
- package/behaviors/LinkProvider.d.ts +3 -0
- package/behaviors/LocaleProvider.d.ts +27 -0
- package/behaviors/LocaleProvider.js +16 -0
- package/behaviors/ScrollRegion.d.ts +8 -0
- package/behaviors/ScrollRegion.js +51 -0
- package/behaviors/devWarning.d.ts +9 -0
- package/behaviors/devWarning.js +16 -0
- package/behaviors/direction.css +19 -0
- package/behaviors/direction.d.ts +40 -0
- package/behaviors/direction.js +56 -0
- package/behaviors/focusable.d.ts +2 -1
- package/behaviors/locale.d.ts +47 -0
- package/behaviors/locale.js +87 -0
- package/behaviors/shortcut-keys.d.ts +49 -0
- package/behaviors/shortcut-keys.js +129 -0
- package/behaviors/shortcuts.d.ts +75 -0
- package/behaviors/shortcuts.js +160 -0
- package/behaviors/useAnchoredPosition.d.ts +124 -0
- package/behaviors/useAnchoredPosition.js +158 -0
- package/behaviors/useFocusScope.d.ts +15 -2
- package/behaviors/useFocusScope.js +7 -1
- package/behaviors/useLayer.d.ts +9 -0
- package/behaviors/useLayer.js +4 -0
- package/behaviors/useScrollLock.d.ts +4 -0
- package/bin/rift-ds.mjs +184 -25
- package/bin/templates/app/components/rift/ColorModeScript.tsx +10 -0
- package/bin/templates/app/components/rift/RiftRoot.tsx +13 -0
- package/bin/templates/app/components/rift/font.css +7 -0
- package/bin/templates/app/components/rift/font.ts +14 -0
- package/bin/templates/blocks/app/dashboard/page.module.css +90 -0
- package/bin/templates/blocks/app/dashboard/page.tsx +141 -0
- package/bin/templates/blocks/app/sign-in/page.module.css +91 -0
- package/bin/templates/blocks/app/sign-in/page.tsx +95 -0
- package/bin/templates/blocks/app/starter/page.module.css +7 -0
- package/bin/templates/blocks/app/starter/page.tsx +31 -0
- package/bin/templates/blocks/components/blocks/AppShell.module.css +160 -0
- package/bin/templates/blocks/components/blocks/AppShell.tsx +113 -0
- package/bin/templates/blocks/registry.json +39 -0
- package/chat/AssistantTurn.js +4 -2
- package/chat/ChatWidget.css +24 -24
- package/chat/ChatWidget.js +4 -2
- package/chat/suggestions.d.ts +12 -0
- package/chat/types.d.ts +8 -7
- package/chat/useChat.js +2 -2
- package/components/Accordion/Accordion.css +1 -1
- package/components/AgentPlan/AgentPlan.css +2 -2
- package/components/AgentRail/AgentRail.css +4 -4
- package/components/AgentRail/AgentRail.js +1 -1
- package/components/AgentStatus/AgentRing.d.ts +27 -0
- package/components/AgentStatus/AgentRing.js +9 -0
- package/components/AgentStatus/AgentStatus.css +638 -10
- package/components/AgentStatus/AgentStatus.d.ts +31 -3
- package/components/AgentStatus/AgentStatus.js +22 -4
- package/components/AgentStatus/AgentStatusPatterns.d.ts +1 -1
- package/components/AgentStatus/AgentStatusPatterns.js +2 -1
- package/components/AiButton/AiButton.css +214 -41
- package/components/AiButton/AiButton.d.ts +34 -0
- package/components/AiButton/AiButton.js +130 -9
- package/components/AlertDialog/AlertDialog.css +1 -1
- package/components/AnchorNav/AnchorNav.css +6 -6
- package/components/AnchorNav/AnchorNav.js +4 -2
- package/components/AnimatedNumber/AnimatedNumber.d.ts +6 -0
- package/components/AnimatedNumber/AnimatedNumber.js +12 -5
- package/components/AppLayout/AppLayout.css +3 -3
- package/components/AppSidebar/AppSidebar.css +20 -20
- package/components/AppSidebar/AppSidebar.js +6 -4
- package/components/ArtifactCard/ArtifactCard.css +1 -1
- package/components/ArtifactCard/ArtifactCard.js +2 -1
- package/components/ArtifactPanel/ArtifactPanel.js +2 -1
- package/components/Attachment/Attachment.css +4 -4
- package/components/Attachment/AttachmentDropZone.js +2 -1
- package/components/Attachment/AttachmentGroup.d.ts +2 -0
- package/components/Attachment/AttachmentGroup.js +4 -1
- package/components/Attachment/AttachmentViewer.d.ts +2 -0
- package/components/Attachment/AttachmentViewer.js +8 -13
- package/components/Attachment/fileTypes.d.ts +5 -3
- package/components/Attachment/fileTypes.js +8 -3
- package/components/Avatar/Avatar.css +55 -1
- package/components/Avatar/Avatar.d.ts +9 -1
- package/components/Avatar/Avatar.js +5 -1
- package/components/Avatar/avatarFill.d.ts +9 -0
- package/components/Avatar/avatarFill.js +13 -0
- package/components/AvatarGroup/AvatarGroup.css +1 -1
- package/components/AvatarGroup/AvatarGroup.js +13 -1
- package/components/BentoTile/BentoTile.css +15 -3
- package/components/BottomNav/BottomNav.css +8 -8
- package/components/BottomNav/BottomNav.js +3 -1
- package/components/Breadcrumb/Breadcrumb.js +2 -2
- package/components/Button/Button.css +8 -1
- package/components/Button/Button.d.ts +8 -1
- package/components/Button/Button.js +14 -3
- package/components/Card/Card.css +3 -3
- package/components/CardStack/CardStack.js +4 -2
- package/components/Carousel/Carousel.css +5 -2
- package/components/Carousel/Carousel.js +11 -7
- package/components/Chart/AreaChart.d.ts +2 -0
- package/components/Chart/AreaChart.js +15 -6
- package/components/Chart/BarChart.d.ts +2 -0
- package/components/Chart/BarChart.js +15 -5
- package/components/Chart/Chart.css +6 -6
- package/components/Chart/ComboChart.d.ts +2 -0
- package/components/Chart/ComboChart.js +16 -6
- package/components/Chart/LineChart.d.ts +2 -0
- package/components/Chart/LineChart.js +15 -6
- package/components/Chart/PieChart.d.ts +2 -0
- package/components/Chart/PieChart.js +9 -5
- package/components/Chart/RadarChart.d.ts +2 -0
- package/components/Chart/RadarChart.js +15 -6
- package/components/Chart/RadialChart.d.ts +2 -0
- package/components/Chart/RadialChart.js +9 -5
- package/components/Chart/ScatterChart.d.ts +2 -0
- package/components/Chart/ScatterChart.js +17 -7
- package/components/Chart/StackedBarChart.d.ts +2 -0
- package/components/Chart/StackedBarChart.js +15 -7
- package/components/Chart/Treemap.d.ts +2 -0
- package/components/Chart/Treemap.js +9 -5
- package/components/ChatMarker/ChatMarker.js +2 -1
- package/components/ChatMessage/ChatMessage.css +2 -2
- package/components/ChatThread/ChatThread.css +1 -1
- package/components/Checkbox/Checkbox.js +1 -1
- package/components/CircularButton/CircularButton.css +8 -7
- package/components/CircularButton/CircularButton.d.ts +2 -2
- package/components/CircularButton/CircularButton.js +2 -1
- package/components/CodeBlock/CodeBlock.d.ts +2 -0
- package/components/CodeBlock/CodeBlock.js +2 -0
- package/components/CodeDiff/CodeDiff.css +2 -2
- package/components/ColorPicker/ColorPicker.css +8 -8
- package/components/ColorPicker/ColorPicker.js +11 -0
- package/components/ColourSwatch/ColourSwatch.css +1 -1
- package/components/Combobox/Combobox.css +20 -5
- package/components/Combobox/Combobox.js +75 -55
- package/components/CommandPalette/CommandPalette.d.ts +13 -1
- package/components/CommandPalette/CommandPalette.js +12 -13
- package/components/CommandPaletteTrigger/CommandPaletteTrigger.css +88 -0
- package/components/CommandPaletteTrigger/CommandPaletteTrigger.d.ts +41 -0
- package/components/CommandPaletteTrigger/CommandPaletteTrigger.js +58 -0
- package/components/Composer/Composer.css +7 -7
- package/components/Composer/Composer.d.ts +2 -0
- package/components/Composer/Composer.js +6 -2
- package/components/ContactCard/ContactCard.css +1 -1
- package/components/ContactCard/ContactCard.js +4 -3
- package/components/ContextMenu/ContextMenu.css +10 -7
- package/components/ContextMenu/ContextMenu.d.ts +3 -0
- package/components/ContextMenu/ContextMenu.js +15 -9
- package/components/ContributionGraph/ContributionGraph.css +2 -2
- package/components/ContributionGraph/ContributionGraph.d.ts +7 -1
- package/components/ContributionGraph/ContributionGraph.js +6 -4
- package/components/DataTable/CellEditor.d.ts +37 -0
- package/components/DataTable/CellEditor.js +153 -0
- package/components/DataTable/DataTable.css +1181 -1
- package/components/DataTable/DataTable.d.ts +506 -29
- package/components/DataTable/DataTable.js +2374 -190
- package/components/DataTable/FilterEditor.d.ts +10 -0
- package/components/DataTable/FilterEditor.js +181 -0
- package/components/DataTable/TypedCell.d.ts +22 -0
- package/components/DataTable/TypedCell.js +80 -0
- package/components/DataTable/VirtualRows.d.ts +21 -0
- package/components/DataTable/VirtualRows.js +38 -0
- package/components/DataTable/aggregates.d.ts +29 -0
- package/components/DataTable/aggregates.js +52 -0
- package/components/DataTable/columnTypes.d.ts +49 -0
- package/components/DataTable/columnTypes.js +96 -0
- package/components/DataTable/dragGesture.d.ts +24 -0
- package/components/DataTable/dragGesture.js +62 -0
- package/components/DataTable/editorKinds.d.ts +24 -0
- package/components/DataTable/editorKinds.js +20 -0
- package/components/DataTable/filterModel.d.ts +84 -0
- package/components/DataTable/filterModel.js +159 -0
- package/components/DataTable/tableContext.d.ts +82 -0
- package/components/DataTable/tableContext.js +7 -0
- package/components/DataTable/views.d.ts +50 -0
- package/components/DataTable/views.js +39 -0
- package/components/DateInput/DateInput.css +10 -4
- package/components/DatePicker/DatePicker.d.ts +23 -7
- package/components/DatePicker/DatePicker.js +41 -34
- package/components/Dialog/Dialog.css +1 -1
- package/components/DocumentChip/DocumentChip.css +1 -1
- package/components/DocumentChip/DocumentChip.js +2 -1
- package/components/Drawer/Drawer.css +46 -12
- package/components/Drawer/Drawer.d.ts +6 -2
- package/components/Drawer/Drawer.js +3 -1
- package/components/Dropdown/ChosenMark.css +1 -1
- package/components/Dropdown/Dropdown.css +23 -7
- package/components/Dropdown/Dropdown.d.ts +6 -0
- package/components/Dropdown/Dropdown.js +41 -10
- package/components/DropdownMenu/DropdownMenu.css +38 -17
- package/components/DropdownMenu/DropdownMenu.d.ts +10 -1
- package/components/DropdownMenu/DropdownMenu.js +80 -23
- package/components/DropdownMenu/ShortcutHint.d.ts +12 -0
- package/components/DropdownMenu/ShortcutHint.js +17 -0
- package/components/EmptyState/EmptyState.js +2 -1
- package/components/EntityCard/EntityCard.js +2 -1
- package/components/EventCalendar/EventCalendar.css +1 -1
- package/components/EventCalendar/EventCalendar.d.ts +21 -6
- package/components/EventCalendar/EventCalendar.js +33 -31
- package/components/FileInput/FileInput.d.ts +2 -0
- package/components/FileInput/FileInput.js +4 -1
- package/components/FilterBar/FilterBar.css +10 -6
- package/components/FilterBar/FilterBar.js +69 -44
- package/components/FunnelChart/FunnelChart.d.ts +2 -0
- package/components/FunnelChart/FunnelChart.js +11 -7
- package/components/GanttChart/GanttChart.css +1 -0
- package/components/GanttChart/GanttChart.js +2 -1
- package/components/Gauge/Gauge.css +1 -1
- package/components/Gauge/Gauge.d.ts +2 -0
- package/components/Gauge/Gauge.js +3 -1
- package/components/Globe/Globe.css +1 -0
- package/components/HoverCard/HoverCard.css +25 -44
- package/components/HoverCard/HoverCard.d.ts +2 -2
- package/components/HoverCard/HoverCard.js +18 -2
- package/components/ImageCompare/ImageCompare.css +1 -0
- package/components/Input/Input.css +14 -8
- package/components/Input/Input.js +3 -2
- package/components/Inspector/Inspector.css +12 -11
- package/components/Inspector/InspectorSlider.d.ts +2 -0
- package/components/Inspector/InspectorSlider.js +8 -1
- package/components/Instructions/Instructions.css +1 -1
- package/components/Instructions/Instructions.js +2 -1
- package/components/InterruptCard/InterruptCard.css +9 -1
- package/components/InterruptCard/InterruptCard.d.ts +2 -0
- package/components/InterruptCard/InterruptCard.js +9 -2
- package/components/Kbd/Kbd.css +21 -0
- package/components/Kbd/Kbd.d.ts +14 -4
- package/components/Kbd/Kbd.js +10 -1
- package/components/LegendTile/LegendTile.d.ts +6 -1
- package/components/LegendTile/LegendTile.js +3 -2
- package/components/Lightbox/Lightbox.css +3 -3
- package/components/Lightbox/Lightbox.js +6 -14
- package/components/LinkList/LinkList.js +3 -2
- package/components/MapCallout/MapCallout.css +1 -0
- package/components/Mention/Mention.js +2 -1
- package/components/MessageActions/MessageActions.js +2 -1
- package/components/MessageCard/MessageCard.css +1 -1
- package/components/MessageCard/MessageCard.js +2 -1
- package/components/Meter/Meter.css +1 -1
- package/components/Meter/Meter.d.ts +2 -0
- package/components/Meter/Meter.js +3 -1
- package/components/ModelPicker/ModelPicker.css +14 -11
- package/components/ModelPicker/ModelPicker.d.ts +2 -0
- package/components/ModelPicker/ModelPicker.js +15 -1
- package/components/MotionSwatch/MotionSwatch.css +3 -3
- package/components/NavList/NavList.css +3 -3
- package/components/NotificationCenter/NotificationCenter.d.ts +2 -0
- package/components/NotificationCenter/NotificationCenter.js +7 -2
- package/components/Pagination/Pagination.css +8 -6
- package/components/Pagination/Pagination.d.ts +2 -0
- package/components/Pagination/Pagination.js +6 -2
- package/components/PinInput/PinInput.js +3 -2
- package/components/PixelAvatar/pixelInks.d.ts +6 -0
- package/components/PixelAvatar/pixelInks.js +7 -3
- package/components/Popover/Popover.css +32 -30
- package/components/Popover/Popover.d.ts +10 -2
- package/components/Popover/Popover.js +16 -2
- package/components/ProgressBar/ProgressBar.css +1 -1
- package/components/ProgressBar/ProgressBar.d.ts +2 -0
- package/components/ProgressBar/ProgressBar.js +3 -5
- package/components/PromptSuggestions/PromptSuggestions.css +10 -3
- package/components/PromptSuggestions/PromptSuggestions.js +3 -1
- package/components/Prose/Prose.css +50 -8
- package/components/Prose/Prose.d.ts +2 -0
- package/components/Prose/Prose.js +7 -2
- package/components/QuestionCard/QuestionCard.css +1 -1
- package/components/Quote/Quote.css +4 -4
- package/components/Rating/Rating.js +4 -3
- package/components/Reasoning/Reasoning.css +8 -8
- package/components/Reasoning/Reasoning.js +1 -0
- package/components/RichDropdown/RichDropdown.css +18 -5
- package/components/RichDropdown/RichDropdown.js +13 -0
- package/components/SegmentedControl/SegmentedControl.css +9 -1
- package/components/SegmentedControl/SegmentedControl.js +5 -3
- package/components/ShaderField/useShaderField.js +13 -1
- package/components/SidebarPanel/SidebarPanel.css +7 -7
- package/components/SidebarPanel/SidebarPanel.js +2 -2
- package/components/Skeleton/Skeleton.css +2 -2
- package/components/Skeleton/Skeleton.js +1 -0
- package/components/Slider/Slider.css +16 -1
- package/components/Slider/Slider.js +1 -1
- package/components/SourceChip/SourceChip.css +6 -12
- package/components/SourceChip/SourceChip.d.ts +2 -0
- package/components/SourceChip/SourceChip.js +16 -37
- package/components/SourceTrail/SourceTrail.css +1 -1
- package/components/SpacingSwatch/SpacingSwatch.css +3 -3
- package/components/SplitButton/SplitButton.css +2 -2
- package/components/SplitButton/SplitButton.js +1 -0
- package/components/SplitPane/SplitPane.js +3 -2
- package/components/Stat/Stat.css +1 -1
- package/components/Stat/Stat.d.ts +2 -0
- package/components/Stat/Stat.js +3 -1
- package/components/Stepper/Stepper.css +5 -5
- package/components/Stepper/Stepper.d.ts +2 -0
- package/components/Stepper/Stepper.js +5 -1
- package/components/StreamingText/StreamingText.css +1 -1
- package/components/Table/Table.css +63 -21
- package/components/Table/Table.d.ts +10 -2
- package/components/Table/Table.js +34 -12
- package/components/TableToolbar/TableToolbar.css +494 -0
- package/components/TableToolbar/TableToolbar.d.ts +281 -0
- package/components/TableToolbar/TableToolbar.js +1322 -0
- package/components/Tabs/Tabs.js +3 -2
- package/components/ThemePicker/ThemePicker.css +189 -0
- package/components/ThemePicker/ThemePicker.d.ts +67 -0
- package/components/ThemePicker/ThemePicker.js +165 -0
- package/components/ThemeToggle/ThemeToggle.css +82 -0
- package/components/ThemeToggle/ThemeToggle.d.ts +42 -0
- package/components/ThemeToggle/ThemeToggle.js +98 -0
- package/components/ThemeToggle/colorMode.d.ts +42 -0
- package/components/ThemeToggle/colorMode.js +26 -0
- package/components/ThemeToggle/useColorMode.d.ts +8 -0
- package/components/ThemeToggle/useColorMode.js +44 -0
- package/components/ThreadPanel/ThreadPanel.css +19 -5
- package/components/ThreadPanel/ThreadPanel.d.ts +6 -1
- package/components/ThreadPanel/ThreadPanel.js +11 -7
- package/components/ThreadTabs/ThreadTabs.css +8 -1
- package/components/ThreadTabs/ThreadTabs.js +4 -2
- package/components/TimePicker/TimePicker.css +12 -6
- package/components/TimePicker/TimePicker.d.ts +17 -4
- package/components/TimePicker/TimePicker.js +32 -3
- package/components/Timeline/Timeline.css +8 -8
- package/components/Timeline/Timeline.d.ts +2 -0
- package/components/Timeline/Timeline.js +5 -2
- package/components/Toast/Toast.css +5 -5
- package/components/Toast/Toast.js +3 -2
- package/components/ToggleGroup/ToggleGroup.css +11 -1
- package/components/ToggleSwitch/ToggleSwitch.css +5 -5
- package/components/ToggleSwitch/ToggleSwitch.js +1 -0
- package/components/ToolCall/ToolCall.css +3 -3
- package/components/Toolbar/Toolbar.js +4 -2
- package/components/Tooltip/Tooltip.css +25 -25
- package/components/Tooltip/Tooltip.d.ts +2 -2
- package/components/Tooltip/Tooltip.js +16 -60
- package/components/TopAppBar/TopAppBar.css +7 -5
- package/components/TopAppBar/TopAppBar.js +3 -1
- package/components/TreeView/TreeView.css +1 -1
- package/components/TreeView/TreeView.js +4 -3
- package/components/UsageCard/UsageCard.d.ts +2 -0
- package/components/UsageCard/UsageCard.js +3 -0
- package/components/Widget/Widget.css +1 -1
- package/components/Widget/Widget.d.ts +2 -0
- package/components/Widget/Widget.js +3 -2
- package/components/WorldMap/WorldMap.css +1 -0
- package/components/registry.json +37 -5
- package/components/registry.json.d.ts +37 -5
- package/components/registry.json.js +1 -1
- package/fonts/material-symbols.css +13 -1
- package/fonts/presets/anton-400-latin-ext.woff2 +0 -0
- package/fonts/presets/anton-400-latin.woff2 +0 -0
- package/fonts/presets/work-sans-300-700-latin-ext.woff2 +0 -0
- package/fonts/presets/work-sans-300-700-latin.woff2 +0 -0
- package/index.d.ts +12 -0
- package/index.js +34 -2
- package/package.json +13 -1
- package/tokens/base.css +84 -0
- package/tokens/motion.d.ts +4 -0
- package/tokens/motion.js +4 -0
- package/tokens/presets/contrast.css +6 -2
- package/tokens/presets/coral.css +4 -2
- package/tokens/presets/forest.css +6 -0
- package/tokens/presets/gold.css +10 -2
- package/tokens/presets/marquee.css +299 -0
- package/tokens/presets/mono.css +4 -2
- package/tokens/presets/pink.css +10 -0
- package/tokens/presets/presets.css +1 -0
- package/tokens/presets/terminal.css +8 -2
- package/tokens/presets/violet.css +10 -0
- package/tokens/presets/warm.css +4 -0
- package/tokens/presets/zest.css +6 -0
- package/tokens/registry.d.ts +1 -1
- package/tokens/registry.json +32 -1
- package/tokens/registry.json.d.ts +32 -1
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +53 -0
- package/tokens/tokens-forced-colors.css +31 -0
- package/tokens/tokens-light.css +66 -0
- package/tokens/tokens-primitives.css +9 -0
- package/tokens/tokens.css +1 -0
- package/cli/templates/chat/app/api/chat/feedback/route.d.ts +0 -3
- package/cli/templates/chat/app/api/chat/followups/route.d.ts +0 -3
- package/cli/templates/chat/app/api/chat/guardrails.d.ts +0 -10
- package/cli/templates/chat/app/api/chat/route.d.ts +0 -4
- package/cli/templates/chat/components/chat/ChatMount.d.ts +0 -1
- package/cli/templates/chat/components/chat/models.d.ts +0 -11
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const KEY_ALIASES = {
|
|
2
|
+
esc: "escape",
|
|
3
|
+
return: "enter",
|
|
4
|
+
up: "arrowup",
|
|
5
|
+
down: "arrowdown",
|
|
6
|
+
left: "arrowleft",
|
|
7
|
+
right: "arrowright",
|
|
8
|
+
del: "delete",
|
|
9
|
+
plus: "+",
|
|
10
|
+
" ": "space",
|
|
11
|
+
cmd: "meta",
|
|
12
|
+
command: "meta",
|
|
13
|
+
option: "alt",
|
|
14
|
+
control: "ctrl"
|
|
15
|
+
};
|
|
16
|
+
const MODIFIERS = /* @__PURE__ */ new Set(["mod", "ctrl", "meta", "alt", "shift"]);
|
|
17
|
+
const parseStep = (text) => {
|
|
18
|
+
const step = { key: "", mod: false, ctrl: false, meta: false, alt: false, shift: false };
|
|
19
|
+
for (const raw of text.toLowerCase().split("+")) {
|
|
20
|
+
const part = KEY_ALIASES[raw] ?? raw;
|
|
21
|
+
if (MODIFIERS.has(part)) step[part] = true;
|
|
22
|
+
else step.key = part;
|
|
23
|
+
}
|
|
24
|
+
return step;
|
|
25
|
+
};
|
|
26
|
+
const parseShortcut = (shortcut) => shortcut.trim().split(/\s+/).filter(Boolean).map(parseStep);
|
|
27
|
+
const detectPlatform = () => {
|
|
28
|
+
if (typeof navigator === "undefined") return "other";
|
|
29
|
+
const nav = navigator;
|
|
30
|
+
const name = nav.userAgentData?.platform || nav.platform || nav.userAgent || "";
|
|
31
|
+
return /mac|iphone|ipad|ipod/i.test(name) ? "apple" : "other";
|
|
32
|
+
};
|
|
33
|
+
const APPLE_LABELS = {
|
|
34
|
+
mod: "⌘",
|
|
35
|
+
meta: "⌘",
|
|
36
|
+
ctrl: "⌃",
|
|
37
|
+
alt: "⌥",
|
|
38
|
+
shift: "⇧",
|
|
39
|
+
enter: "↩",
|
|
40
|
+
escape: "Esc",
|
|
41
|
+
backspace: "⌫",
|
|
42
|
+
delete: "⌦",
|
|
43
|
+
tab: "⇥",
|
|
44
|
+
space: "Space"
|
|
45
|
+
};
|
|
46
|
+
const OTHER_LABELS = {
|
|
47
|
+
mod: "Ctrl",
|
|
48
|
+
meta: "Meta",
|
|
49
|
+
ctrl: "Ctrl",
|
|
50
|
+
alt: "Alt",
|
|
51
|
+
shift: "Shift",
|
|
52
|
+
enter: "Enter",
|
|
53
|
+
escape: "Esc",
|
|
54
|
+
backspace: "Backspace",
|
|
55
|
+
delete: "Del",
|
|
56
|
+
tab: "Tab",
|
|
57
|
+
space: "Space"
|
|
58
|
+
};
|
|
59
|
+
const SHARED_LABELS = {
|
|
60
|
+
arrowup: "↑",
|
|
61
|
+
arrowdown: "↓",
|
|
62
|
+
arrowleft: "←",
|
|
63
|
+
arrowright: "→",
|
|
64
|
+
home: "Home",
|
|
65
|
+
end: "End",
|
|
66
|
+
pageup: "PgUp",
|
|
67
|
+
pagedown: "PgDn"
|
|
68
|
+
};
|
|
69
|
+
const APPLE_ORDER = ["ctrl", "alt", "shift", "mod", "meta"];
|
|
70
|
+
const OTHER_ORDER = ["mod", "ctrl", "meta", "alt", "shift"];
|
|
71
|
+
function shortcutKeys(shortcut, platform, labels = {}) {
|
|
72
|
+
const table = { ...SHARED_LABELS, ...platform === "apple" ? APPLE_LABELS : OTHER_LABELS };
|
|
73
|
+
const legend = (name) => labels[name] ?? table[name] ?? (/^f\d+$/.test(name) ? name.toUpperCase() : name.length === 1 ? name.toUpperCase() : name);
|
|
74
|
+
return parseShortcut(shortcut).map((step) => {
|
|
75
|
+
const order = platform === "apple" ? APPLE_ORDER : OTHER_ORDER;
|
|
76
|
+
const mods = order.filter((mod) => step[mod]);
|
|
77
|
+
const unique = mods.filter((mod, i) => !mods.slice(0, i).some((m) => legend(m) === legend(mod)));
|
|
78
|
+
return [...unique.map(legend), legend(step.key)];
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function formatShortcut(shortcut, platform, labels = {}) {
|
|
82
|
+
return shortcutKeys(shortcut, platform, labels).map((keys) => keys.join(platform === "apple" ? "" : "+")).join(" ");
|
|
83
|
+
}
|
|
84
|
+
const ARIA_KEYS = {
|
|
85
|
+
ctrl: "Control",
|
|
86
|
+
meta: "Meta",
|
|
87
|
+
alt: "Alt",
|
|
88
|
+
shift: "Shift",
|
|
89
|
+
escape: "Escape",
|
|
90
|
+
enter: "Enter",
|
|
91
|
+
space: "Space",
|
|
92
|
+
tab: "Tab",
|
|
93
|
+
backspace: "Backspace",
|
|
94
|
+
delete: "Delete",
|
|
95
|
+
arrowup: "ArrowUp",
|
|
96
|
+
arrowdown: "ArrowDown",
|
|
97
|
+
arrowleft: "ArrowLeft",
|
|
98
|
+
arrowright: "ArrowRight",
|
|
99
|
+
home: "Home",
|
|
100
|
+
end: "End",
|
|
101
|
+
pageup: "PageUp",
|
|
102
|
+
pagedown: "PageDown"
|
|
103
|
+
};
|
|
104
|
+
function shortcutAria(shortcut, platform) {
|
|
105
|
+
const steps = parseShortcut(shortcut);
|
|
106
|
+
if (steps.length !== 1) return void 0;
|
|
107
|
+
const [step] = steps;
|
|
108
|
+
const names = [
|
|
109
|
+
step.ctrl || step.mod && platform === "other" ? "Control" : null,
|
|
110
|
+
step.meta || step.mod && platform === "apple" ? "Meta" : null,
|
|
111
|
+
step.alt ? "Alt" : null,
|
|
112
|
+
step.shift ? "Shift" : null,
|
|
113
|
+
ARIA_KEYS[step.key] ?? (step.key.length === 1 ? step.key.toUpperCase() : step.key)
|
|
114
|
+
].filter(Boolean);
|
|
115
|
+
return names.join("+");
|
|
116
|
+
}
|
|
117
|
+
function shortcutAriaAnyPlatform(shortcut) {
|
|
118
|
+
const forms = [shortcutAria(shortcut, "apple"), shortcutAria(shortcut, "other")].filter(Boolean);
|
|
119
|
+
return forms.length > 0 ? [...new Set(forms)].join(" ") : void 0;
|
|
120
|
+
}
|
|
121
|
+
export {
|
|
122
|
+
KEY_ALIASES,
|
|
123
|
+
detectPlatform,
|
|
124
|
+
formatShortcut,
|
|
125
|
+
parseShortcut,
|
|
126
|
+
shortcutAria,
|
|
127
|
+
shortcutAriaAnyPlatform,
|
|
128
|
+
shortcutKeys
|
|
129
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Shortcut, ShortcutPlatform } from './shortcut-keys';
|
|
2
|
+
export { detectPlatform, formatShortcut, parseShortcut, shortcutAria, shortcutKeys, type Shortcut, type ShortcutKeyLabels, type ShortcutPlatform, } from './shortcut-keys';
|
|
3
|
+
/**
|
|
4
|
+
* Keyboard shortcuts: the one document keydown listener every app-level key
|
|
5
|
+
* binding goes through, the grammar a shortcut is written in, and the way it
|
|
6
|
+
* is drawn on each platform.
|
|
7
|
+
*
|
|
8
|
+
* A shortcut is a string: steps separated by spaces, each step modifiers and
|
|
9
|
+
* a key joined by `+`. `mod+k` is ⌘K on Apple platforms and Ctrl+K
|
|
10
|
+
* elsewhere; `shift+?`, `alt+arrowup`, `escape`; `g i` is G then I. Keys are
|
|
11
|
+
* a character or a name (enter, escape, space, tab, backspace, delete, the
|
|
12
|
+
* arrows, home, end, pageup, pagedown, f1 to f12, plus).
|
|
13
|
+
*
|
|
14
|
+
* Who hears a key, in order:
|
|
15
|
+
* - Nothing does while an IME is composing, or once a component has handled
|
|
16
|
+
* the key itself (`event.preventDefault()`), so a list's own arrow keys or
|
|
17
|
+
* an input's own Escape never also fire a shortcut. Escape is the layer
|
|
18
|
+
* stack's while any overlay is open (useLayer), so an `escape` shortcut
|
|
19
|
+
* is for closing something that is not a library overlay.
|
|
20
|
+
* - Nothing does while focus is in a text field, unless the shortcut opts in
|
|
21
|
+
* with `allowInInput` (a palette's Cmd+K should work from a search box).
|
|
22
|
+
* - A `global` shortcut always does.
|
|
23
|
+
* - A `scope`d shortcut does while focus is inside its element, or while
|
|
24
|
+
* nothing has focus and the element sits on the surface in front (the open
|
|
25
|
+
* modal, else the page). Lightbox's arrows are scoped to its panel.
|
|
26
|
+
* - Any other shortcut belongs to the page: it does only while no modal is
|
|
27
|
+
* open and focus is not inside an open overlay (a menu, a popover), the
|
|
28
|
+
* same topmost-only rule the layer stack applies to Escape.
|
|
29
|
+
* When several match, the longest sequence wins, then the shortcut
|
|
30
|
+
* registered last. A matched key's default is prevented.
|
|
31
|
+
*
|
|
32
|
+
* Exactly one document listener exists while any shortcut is registered, and
|
|
33
|
+
* none otherwise. scripts/validate-overlay-listeners.mjs holds every other
|
|
34
|
+
* document key listener in the library and the site to this module.
|
|
35
|
+
*/
|
|
36
|
+
export interface ShortcutOptions {
|
|
37
|
+
/** What the shortcut does, in a few words, for a help list built from `useRegisteredShortcuts`. */
|
|
38
|
+
description?: string;
|
|
39
|
+
/** The heading a help list files the shortcut under, such as "Navigation". */
|
|
40
|
+
group?: string;
|
|
41
|
+
/** Whether the shortcut is live. Default true. */
|
|
42
|
+
enabled?: boolean;
|
|
43
|
+
/** Fires only while focus is inside this element (see the module's rules). */
|
|
44
|
+
scope?: React.RefObject<HTMLElement | null>;
|
|
45
|
+
/** Fires whatever is open or focused, outside text fields. Default false. */
|
|
46
|
+
global?: boolean;
|
|
47
|
+
/** Fires while focus is in a text field too. Default false. */
|
|
48
|
+
allowInInput?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The platform shortcuts are drawn for. The server and the first client
|
|
52
|
+
* render both say `other`, then the client settles on its own, so drawing a
|
|
53
|
+
* shortcut never breaks hydration.
|
|
54
|
+
*/
|
|
55
|
+
export declare function usePlatform(): ShortcutPlatform;
|
|
56
|
+
/**
|
|
57
|
+
* Binds a shortcut (or several, as an array) to `handler` while the component
|
|
58
|
+
* is mounted and `enabled` is not false. The handler and options are read at
|
|
59
|
+
* key time, so passing new ones each render never re-registers the shortcut.
|
|
60
|
+
*/
|
|
61
|
+
export declare function useShortcut(shortcut: Shortcut | Shortcut[], handler: (event: KeyboardEvent) => void, options?: ShortcutOptions): void;
|
|
62
|
+
/** A live shortcut as a help list reads it. */
|
|
63
|
+
export interface RegisteredShortcut {
|
|
64
|
+
/** The shortcuts that run it, in the grammar. */
|
|
65
|
+
shortcuts: Shortcut[];
|
|
66
|
+
/** What it does, from the binding's `description`. */
|
|
67
|
+
description: string;
|
|
68
|
+
/** Its heading, from the binding's `group`. */
|
|
69
|
+
group?: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Every live shortcut that has a description, in registration order, for a
|
|
73
|
+
* "keyboard shortcuts" help dialog. Updates as shortcuts mount and unmount.
|
|
74
|
+
*/
|
|
75
|
+
export declare function useRegisteredShortcuts(): RegisteredShortcut[];
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useSyncExternalStore, useRef, useEffect } from "react";
|
|
3
|
+
import { MOTION_SHORTCUT_SEQUENCE_MS } from "../tokens/motion.js";
|
|
4
|
+
import { warnOnce } from "./devWarning.js";
|
|
5
|
+
import { activeModalPanel } from "./useFocusScope.js";
|
|
6
|
+
import { hasOpenLayer, openLayerContains } from "./useLayer.js";
|
|
7
|
+
import { detectPlatform, parseShortcut, KEY_ALIASES } from "./shortcut-keys.js";
|
|
8
|
+
import { formatShortcut, shortcutAria, shortcutKeys } from "./shortcut-keys.js";
|
|
9
|
+
const noSubscription = () => () => {
|
|
10
|
+
};
|
|
11
|
+
function usePlatform() {
|
|
12
|
+
return useSyncExternalStore(noSubscription, detectPlatform, () => "other");
|
|
13
|
+
}
|
|
14
|
+
const eventKey = (event) => {
|
|
15
|
+
const key = event.key.toLowerCase();
|
|
16
|
+
return KEY_ALIASES[key] ?? key;
|
|
17
|
+
};
|
|
18
|
+
const codeKey = (event) => {
|
|
19
|
+
const match = /^(?:Key([A-Z])|Digit(\d))$/.exec(event.code ?? "");
|
|
20
|
+
return match ? (match[1] ?? match[2]).toLowerCase() : null;
|
|
21
|
+
};
|
|
22
|
+
const matchesStep = (step, event, platform) => {
|
|
23
|
+
const ctrl = step.ctrl || step.mod && platform === "other";
|
|
24
|
+
const meta = step.meta || step.mod && platform === "apple";
|
|
25
|
+
if (event.ctrlKey !== ctrl || event.metaKey !== meta || event.altKey !== step.alt) return false;
|
|
26
|
+
const symbol = step.key.length === 1 && !/[a-z0-9]/.test(step.key);
|
|
27
|
+
if (!symbol && event.shiftKey !== step.shift) return false;
|
|
28
|
+
return eventKey(event) === step.key || codeKey(event) === step.key;
|
|
29
|
+
};
|
|
30
|
+
const isTextField = (target) => {
|
|
31
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
32
|
+
if (target.isContentEditable) return true;
|
|
33
|
+
if (target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) return true;
|
|
34
|
+
if (target instanceof HTMLInputElement) {
|
|
35
|
+
return !["checkbox", "radio", "button", "submit", "reset", "range", "color", "file", "image"].includes(target.type);
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
};
|
|
39
|
+
const registrations = [];
|
|
40
|
+
let version = 0;
|
|
41
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
42
|
+
const notify = () => {
|
|
43
|
+
version += 1;
|
|
44
|
+
for (const listener of listeners) listener();
|
|
45
|
+
};
|
|
46
|
+
let recent = [];
|
|
47
|
+
const eligible = (registration, target) => {
|
|
48
|
+
const options = registration.optionsRef.current;
|
|
49
|
+
if (options.enabled === false) return false;
|
|
50
|
+
if (isTextField(target) && !options.allowInInput) return false;
|
|
51
|
+
if (options.global) return true;
|
|
52
|
+
const modal = activeModalPanel();
|
|
53
|
+
const node = target instanceof Node ? target : null;
|
|
54
|
+
const unfocused = !node || node === document.body || node === document.documentElement;
|
|
55
|
+
const scope = options.scope?.current;
|
|
56
|
+
if (scope) {
|
|
57
|
+
if (!unfocused) return scope.contains(node);
|
|
58
|
+
return modal ? modal.contains(scope) : !openLayerContains(scope);
|
|
59
|
+
}
|
|
60
|
+
if (modal) return false;
|
|
61
|
+
return unfocused || !openLayerContains(node);
|
|
62
|
+
};
|
|
63
|
+
const handleKeyDown = (event) => {
|
|
64
|
+
if (event.isComposing || event.defaultPrevented) return;
|
|
65
|
+
if (event.key === "Escape" && hasOpenLayer()) return;
|
|
66
|
+
if (["Shift", "Control", "Alt", "Meta"].includes(event.key)) return;
|
|
67
|
+
const now = event.timeStamp || performance.now();
|
|
68
|
+
const last = recent[recent.length - 1];
|
|
69
|
+
if (last && now - last.at > MOTION_SHORTCUT_SEQUENCE_MS) recent = [];
|
|
70
|
+
recent = [...recent.slice(-3), { event, at: now }];
|
|
71
|
+
const platform = detectPlatform();
|
|
72
|
+
let best = null;
|
|
73
|
+
registrations.forEach((registration2, index) => {
|
|
74
|
+
if (!eligible(registration2, event.target)) return;
|
|
75
|
+
for (const steps of registration2.steps) {
|
|
76
|
+
if (steps.length > recent.length) continue;
|
|
77
|
+
const tail = recent.slice(recent.length - steps.length);
|
|
78
|
+
if (!steps.every((step, i) => matchesStep(step, tail[i].event, platform))) continue;
|
|
79
|
+
if (!best || steps.length > best.length || steps.length === best.length && index > best.index) {
|
|
80
|
+
best = { registration: registration2, length: steps.length, index };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
if (!best) return;
|
|
85
|
+
const { registration } = best;
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
recent = [];
|
|
88
|
+
registration.handlerRef.current(event);
|
|
89
|
+
};
|
|
90
|
+
const register = (registration) => {
|
|
91
|
+
for (const shortcut of registration.shortcuts) {
|
|
92
|
+
const scope = registration.optionsRef.current.scope;
|
|
93
|
+
const clash = registrations.find(
|
|
94
|
+
(other) => other.optionsRef.current.enabled !== false && other.optionsRef.current.scope === scope && other.shortcuts.includes(shortcut)
|
|
95
|
+
);
|
|
96
|
+
if (clash && registration.optionsRef.current.enabled !== false) {
|
|
97
|
+
warnOnce(`The shortcut "${shortcut}" is registered twice in one scope; the one registered last wins.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
registrations.push(registration);
|
|
101
|
+
if (registrations.length === 1) document.addEventListener("keydown", handleKeyDown);
|
|
102
|
+
notify();
|
|
103
|
+
return () => {
|
|
104
|
+
const index = registrations.indexOf(registration);
|
|
105
|
+
if (index !== -1) registrations.splice(index, 1);
|
|
106
|
+
if (registrations.length === 0) {
|
|
107
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
108
|
+
recent = [];
|
|
109
|
+
}
|
|
110
|
+
notify();
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
function useShortcut(shortcut, handler, options = {}) {
|
|
114
|
+
const handlerRef = useRef(handler);
|
|
115
|
+
const optionsRef = useRef(options);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
handlerRef.current = handler;
|
|
118
|
+
optionsRef.current = options;
|
|
119
|
+
});
|
|
120
|
+
const shortcuts = Array.isArray(shortcut) ? shortcut : [shortcut];
|
|
121
|
+
const key = shortcuts.join("\0");
|
|
122
|
+
const enabled = options.enabled !== false;
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (!enabled) return;
|
|
125
|
+
const list = key.split("\0");
|
|
126
|
+
return register({ shortcuts: list, steps: list.map(parseShortcut), handlerRef, optionsRef });
|
|
127
|
+
}, [key, enabled]);
|
|
128
|
+
}
|
|
129
|
+
const subscribe = (listener) => {
|
|
130
|
+
listeners.add(listener);
|
|
131
|
+
return () => listeners.delete(listener);
|
|
132
|
+
};
|
|
133
|
+
let snapshot = { version: -1, list: [] };
|
|
134
|
+
const readRegistered = () => {
|
|
135
|
+
if (snapshot.version !== version) {
|
|
136
|
+
snapshot = {
|
|
137
|
+
version,
|
|
138
|
+
list: registrations.filter((r) => r.optionsRef.current.enabled !== false && r.optionsRef.current.description).map((r) => ({
|
|
139
|
+
shortcuts: r.shortcuts,
|
|
140
|
+
description: r.optionsRef.current.description ?? "",
|
|
141
|
+
group: r.optionsRef.current.group
|
|
142
|
+
}))
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return snapshot.list;
|
|
146
|
+
};
|
|
147
|
+
const EMPTY = [];
|
|
148
|
+
function useRegisteredShortcuts() {
|
|
149
|
+
return useSyncExternalStore(subscribe, readRegistered, () => EMPTY);
|
|
150
|
+
}
|
|
151
|
+
export {
|
|
152
|
+
detectPlatform,
|
|
153
|
+
formatShortcut,
|
|
154
|
+
parseShortcut,
|
|
155
|
+
shortcutAria,
|
|
156
|
+
shortcutKeys,
|
|
157
|
+
usePlatform,
|
|
158
|
+
useRegisteredShortcuts,
|
|
159
|
+
useShortcut
|
|
160
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anchored positioning: the one way every popup in the library places itself
|
|
3
|
+
* against the control that opened it (menus, listboxes, pickers, popovers,
|
|
4
|
+
* hover cards, tooltips).
|
|
5
|
+
*
|
|
6
|
+
* The panel stays where it is in the DOM, so it keeps the trigger's stacking
|
|
7
|
+
* context and any theme scoped above it, but is `position: fixed`, placed
|
|
8
|
+
* from the anchor's rectangle. A fixed panel is not clipped by an ancestor's
|
|
9
|
+
* `overflow`, which is what lets a menu inside a table's scroll wrapper, a
|
|
10
|
+
* rail or a card show whole. Portals would escape the same way but would
|
|
11
|
+
* leave the theme scope; floating-ui would solve it at the cost of a
|
|
12
|
+
* dependency. This is the approach Tooltip proved first.
|
|
13
|
+
*
|
|
14
|
+
* On every placement it:
|
|
15
|
+
* - puts the panel on the preferred side, flipping to the opposite side when
|
|
16
|
+
* the preferred one has too little room and the opposite has more;
|
|
17
|
+
* - shifts it along the edge to stay inside its box, a margin clear of the
|
|
18
|
+
* edge (`--gap-200` unless the component says otherwise);
|
|
19
|
+
* - optionally caps its height to the room on the side it took (a list then
|
|
20
|
+
* scrolls inside rather than running off the screen);
|
|
21
|
+
* - optionally sets its minimum width to the anchor's, so a listbox is never
|
|
22
|
+
* narrower than the control that opened it, and always publishes the
|
|
23
|
+
* anchor's width as `--ds-anchor-width` for a panel that should match it
|
|
24
|
+
* exactly;
|
|
25
|
+
* - marks the side it landed on as `data-side` (and `data-align`), so the
|
|
26
|
+
* entrance can grow from the anchor whichever way the panel flipped;
|
|
27
|
+
* - hides it (an inline `visibility: hidden`, flagged `data-anchor-hidden`)
|
|
28
|
+
* while the anchor is scrolled wholly out of view, so a panel never hangs
|
|
29
|
+
* at the screen's edge over nothing.
|
|
30
|
+
*
|
|
31
|
+
* The placement outlives the open state on purpose: a panel that eases out
|
|
32
|
+
* when it closes keeps its place for the exit instead of snapping back into
|
|
33
|
+
* flow mid-fade.
|
|
34
|
+
*
|
|
35
|
+
* It re-places on scroll (any scroll container, captured), on resize, and
|
|
36
|
+
* when the panel or the anchor changes size, once per animation frame, and
|
|
37
|
+
* writes straight to the DOM: placement never costs a React render, the same
|
|
38
|
+
* reasoning the gesture rule gives (design.md, the playground's resize
|
|
39
|
+
* record).
|
|
40
|
+
*
|
|
41
|
+
* The room a panel has is the viewport's. An ancestor with a transform,
|
|
42
|
+
* filter, backdrop filter, perspective or containment (a Dialog panel's
|
|
43
|
+
* entrance scale, the chat panel's blur) becomes the fixed panel's
|
|
44
|
+
* containing block, which moves the
|
|
45
|
+
* coordinates it is placed in; only when that ancestor also clips its
|
|
46
|
+
* overflow does the room narrow to its box.
|
|
47
|
+
*/
|
|
48
|
+
export type AnchoredSide = 'top' | 'bottom' | 'left' | 'right';
|
|
49
|
+
/** A side a caller can ask for: a screen side, or `start` / `end` of the reading direction. */
|
|
50
|
+
export type AnchoredSideRequest = AnchoredSide | 'start' | 'end';
|
|
51
|
+
export type AnchoredAlign = 'start' | 'center' | 'end';
|
|
52
|
+
export type AnchoredPlacement = {
|
|
53
|
+
/** The side the panel took after flipping. */
|
|
54
|
+
side: AnchoredSide;
|
|
55
|
+
/** The panel's position in the box, in px from the box's top left. */
|
|
56
|
+
x: number;
|
|
57
|
+
y: number;
|
|
58
|
+
/** The panel's size as placed. */
|
|
59
|
+
width: number;
|
|
60
|
+
height: number;
|
|
61
|
+
/** The anchor's rectangle, in the same coordinates. */
|
|
62
|
+
anchor: {
|
|
63
|
+
left: number;
|
|
64
|
+
top: number;
|
|
65
|
+
width: number;
|
|
66
|
+
height: number;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
export type AnchoredPositionOptions = {
|
|
70
|
+
/** Whether the panel is showing. Placement runs only while true. */
|
|
71
|
+
open: boolean;
|
|
72
|
+
/** The control the panel is anchored to. */
|
|
73
|
+
anchorRef: React.RefObject<HTMLElement | null>;
|
|
74
|
+
/** The panel being placed. */
|
|
75
|
+
panelRef: React.RefObject<HTMLElement | null>;
|
|
76
|
+
/**
|
|
77
|
+
* The preferred side. Default `bottom`. `start` and `end` resolve against
|
|
78
|
+
* the anchor's reading direction at placement time (`start` is the left
|
|
79
|
+
* in English, the right in Arabic); the placement reports the screen side
|
|
80
|
+
* it took.
|
|
81
|
+
*/
|
|
82
|
+
side?: AnchoredSideRequest;
|
|
83
|
+
/**
|
|
84
|
+
* Where along that side the panel lines up with the anchor. Default
|
|
85
|
+
* `start`. On the top and bottom sides `start` and `end` follow the
|
|
86
|
+
* anchor's reading direction, so a menu lines up with its trigger's
|
|
87
|
+
* leading edge in either. The panel's `data-align` reports the screen
|
|
88
|
+
* alignment it took (`start` is the left edge), like `data-side`.
|
|
89
|
+
*/
|
|
90
|
+
align?: AnchoredAlign;
|
|
91
|
+
/**
|
|
92
|
+
* The custom property that holds the gap between anchor and panel, read
|
|
93
|
+
* where it resolves so it is never restated as a number. Default
|
|
94
|
+
* `--gap-200`.
|
|
95
|
+
*/
|
|
96
|
+
gapProperty?: string;
|
|
97
|
+
/**
|
|
98
|
+
* The custom property that holds the margin a panel keeps from the box's
|
|
99
|
+
* edge when it shifts to stay inside. Default `--gap-200`, so a panel never
|
|
100
|
+
* sits tighter to the screen's edge than that, whatever its trigger gap.
|
|
101
|
+
*/
|
|
102
|
+
edgeProperty?: string;
|
|
103
|
+
/** Whether the panel may flip to the opposite side. Default true. */
|
|
104
|
+
flip?: boolean;
|
|
105
|
+
/** Cap the panel's height to the room on its side, for a list that scrolls inside. Default false. */
|
|
106
|
+
fitHeight?: boolean;
|
|
107
|
+
/** Set the panel's minimum width to the anchor's. Default false. */
|
|
108
|
+
matchWidth?: boolean;
|
|
109
|
+
/** Called after every placement, for a detail the panel draws itself (an arrow). */
|
|
110
|
+
onPlace?: (placement: AnchoredPlacement) => void;
|
|
111
|
+
/**
|
|
112
|
+
* Which of several anchors is open, for a component that shows one panel
|
|
113
|
+
* at a time against different controls (FilterBar's chips): a change
|
|
114
|
+
* re-reads the refs and places afresh. Leave undefined for one anchor.
|
|
115
|
+
*/
|
|
116
|
+
target?: string | number | null;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* The nearest ancestor a fixed-position descendant is placed from instead of
|
|
120
|
+
* the viewport. Null means the viewport.
|
|
121
|
+
*/
|
|
122
|
+
export declare function fixedContainingBlock(element: HTMLElement): HTMLElement | null;
|
|
123
|
+
/** Places `panelRef` against `anchorRef` while `open`. See the module comment. */
|
|
124
|
+
export declare function useAnchoredPosition({ open, anchorRef, panelRef, side: sideRequest, align: alignRequest, gapProperty, edgeProperty, flip, fitHeight, matchWidth, onPlace, target, }: AnchoredPositionOptions): void;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useRef, useLayoutEffect } from "react";
|
|
3
|
+
import { isRtl } from "./direction.js";
|
|
4
|
+
function fixedContainingBlock(element) {
|
|
5
|
+
for (let node = element.parentElement; node; node = node.parentElement) {
|
|
6
|
+
const style = getComputedStyle(node);
|
|
7
|
+
const read = (property) => style.getPropertyValue(property).trim();
|
|
8
|
+
if (read("transform") !== "none" || read("filter") !== "none" || read("backdrop-filter") !== "none" || read("perspective") !== "none" || /\b(layout|paint|strict|content)\b/.test(read("contain")) || /\b(transform|filter|perspective)\b/.test(read("will-change"))) {
|
|
9
|
+
return node;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const clamp = (value, min, max) => max < min ? min : Math.min(Math.max(value, min), max);
|
|
15
|
+
const OPPOSITE = {
|
|
16
|
+
top: "bottom",
|
|
17
|
+
bottom: "top",
|
|
18
|
+
left: "right",
|
|
19
|
+
right: "left"
|
|
20
|
+
};
|
|
21
|
+
function useAnchoredPosition({
|
|
22
|
+
open,
|
|
23
|
+
anchorRef,
|
|
24
|
+
panelRef,
|
|
25
|
+
side: sideRequest = "bottom",
|
|
26
|
+
align: alignRequest = "start",
|
|
27
|
+
gapProperty = "--gap-200",
|
|
28
|
+
edgeProperty = "--gap-200",
|
|
29
|
+
flip = true,
|
|
30
|
+
fitHeight = false,
|
|
31
|
+
matchWidth = false,
|
|
32
|
+
onPlace,
|
|
33
|
+
target
|
|
34
|
+
}) {
|
|
35
|
+
const onPlaceRef = useRef(onPlace);
|
|
36
|
+
useLayoutEffect(() => {
|
|
37
|
+
onPlaceRef.current = onPlace;
|
|
38
|
+
});
|
|
39
|
+
useLayoutEffect(() => {
|
|
40
|
+
if (!open) return;
|
|
41
|
+
const anchor = anchorRef.current;
|
|
42
|
+
const panel = panelRef.current;
|
|
43
|
+
if (!anchor || !panel) return;
|
|
44
|
+
panel.style.position = "fixed";
|
|
45
|
+
panel.style.inset = "auto";
|
|
46
|
+
panel.style.margin = "0";
|
|
47
|
+
const place = () => {
|
|
48
|
+
const block = fixedContainingBlock(panel);
|
|
49
|
+
const blockBox = block ? block.getBoundingClientRect() : null;
|
|
50
|
+
const origin = blockBox ? { left: blockBox.left + block.clientLeft, top: blockBox.top + block.clientTop } : { left: 0, top: 0 };
|
|
51
|
+
const bounds = {
|
|
52
|
+
left: 0,
|
|
53
|
+
top: 0,
|
|
54
|
+
right: document.documentElement.clientWidth,
|
|
55
|
+
bottom: document.documentElement.clientHeight
|
|
56
|
+
};
|
|
57
|
+
if (block && blockBox) {
|
|
58
|
+
const style = getComputedStyle(block);
|
|
59
|
+
if (style.overflowX !== "visible") {
|
|
60
|
+
bounds.left = Math.max(bounds.left, origin.left);
|
|
61
|
+
bounds.right = Math.min(bounds.right, origin.left + block.clientWidth);
|
|
62
|
+
}
|
|
63
|
+
if (style.overflowY !== "visible") {
|
|
64
|
+
bounds.top = Math.max(bounds.top, origin.top);
|
|
65
|
+
bounds.bottom = Math.min(bounds.bottom, origin.top + block.clientHeight);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const rtl = isRtl(anchor);
|
|
69
|
+
const side = sideRequest === "start" ? rtl ? "right" : "left" : sideRequest === "end" ? rtl ? "left" : "right" : sideRequest;
|
|
70
|
+
const flipsAlign = rtl && (side === "top" || side === "bottom") && alignRequest !== "center";
|
|
71
|
+
const align = flipsAlign ? alignRequest === "start" ? "end" : "start" : alignRequest;
|
|
72
|
+
const a = anchor.getBoundingClientRect();
|
|
73
|
+
const panelStyle = getComputedStyle(panel);
|
|
74
|
+
const gap = parseFloat(panelStyle.getPropertyValue(gapProperty)) || 0;
|
|
75
|
+
const edge = parseFloat(panelStyle.getPropertyValue(edgeProperty)) || 0;
|
|
76
|
+
panel.style.setProperty("--ds-anchor-width", `${a.width}px`);
|
|
77
|
+
if (matchWidth) panel.style.minWidth = `${a.width}px`;
|
|
78
|
+
if (fitHeight) panel.style.maxHeight = "";
|
|
79
|
+
const width = panel.offsetWidth;
|
|
80
|
+
let height = panel.offsetHeight;
|
|
81
|
+
const room = {
|
|
82
|
+
top: a.top - bounds.top - gap - edge,
|
|
83
|
+
bottom: bounds.bottom - a.bottom - gap - edge,
|
|
84
|
+
left: a.left - bounds.left - gap - edge,
|
|
85
|
+
right: bounds.right - a.right - gap - edge
|
|
86
|
+
};
|
|
87
|
+
const vertical = side === "top" || side === "bottom";
|
|
88
|
+
const need = vertical ? height : width;
|
|
89
|
+
let chosen = side;
|
|
90
|
+
if (flip && room[side] < need && room[OPPOSITE[side]] > room[side]) {
|
|
91
|
+
chosen = OPPOSITE[side];
|
|
92
|
+
}
|
|
93
|
+
if (fitHeight && vertical && height > room[chosen]) {
|
|
94
|
+
height = Math.max(room[chosen], 0);
|
|
95
|
+
panel.style.maxHeight = `${height}px`;
|
|
96
|
+
}
|
|
97
|
+
let x;
|
|
98
|
+
let y;
|
|
99
|
+
if (chosen === "top" || chosen === "bottom") {
|
|
100
|
+
y = chosen === "bottom" ? a.bottom + gap : a.top - gap - height;
|
|
101
|
+
x = align === "center" ? a.left + a.width / 2 - width / 2 : align === "end" ? a.right - width : a.left;
|
|
102
|
+
} else {
|
|
103
|
+
x = chosen === "right" ? a.right + gap : a.left - gap - width;
|
|
104
|
+
y = align === "center" ? a.top + a.height / 2 - height / 2 : align === "end" ? a.bottom - height : a.top;
|
|
105
|
+
}
|
|
106
|
+
x = clamp(x, bounds.left + edge, bounds.right - edge - width);
|
|
107
|
+
y = clamp(y, bounds.top + edge, bounds.bottom - edge - height);
|
|
108
|
+
panel.style.left = `${x - origin.left}px`;
|
|
109
|
+
panel.style.top = `${y - origin.top}px`;
|
|
110
|
+
panel.dataset.side = chosen;
|
|
111
|
+
panel.dataset.align = align;
|
|
112
|
+
const offscreen = a.bottom < bounds.top || a.top > bounds.bottom || a.right < bounds.left || a.left > bounds.right;
|
|
113
|
+
if (offscreen) {
|
|
114
|
+
panel.dataset.anchorHidden = "";
|
|
115
|
+
panel.style.visibility = "hidden";
|
|
116
|
+
} else if ("anchorHidden" in panel.dataset) {
|
|
117
|
+
delete panel.dataset.anchorHidden;
|
|
118
|
+
panel.style.removeProperty("visibility");
|
|
119
|
+
}
|
|
120
|
+
onPlaceRef.current?.({
|
|
121
|
+
side: chosen,
|
|
122
|
+
x: x - origin.left,
|
|
123
|
+
y: y - origin.top,
|
|
124
|
+
width,
|
|
125
|
+
height,
|
|
126
|
+
anchor: { left: a.left - origin.left, top: a.top - origin.top, width: a.width, height: a.height }
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
let frame = 0;
|
|
130
|
+
const schedule = () => {
|
|
131
|
+
if (frame) return;
|
|
132
|
+
frame = requestAnimationFrame(() => {
|
|
133
|
+
frame = 0;
|
|
134
|
+
place();
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
place();
|
|
138
|
+
window.addEventListener("scroll", schedule, true);
|
|
139
|
+
window.addEventListener("resize", schedule);
|
|
140
|
+
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(schedule);
|
|
141
|
+
observer?.observe(panel);
|
|
142
|
+
observer?.observe(anchor);
|
|
143
|
+
return () => {
|
|
144
|
+
cancelAnimationFrame(frame);
|
|
145
|
+
window.removeEventListener("scroll", schedule, true);
|
|
146
|
+
window.removeEventListener("resize", schedule);
|
|
147
|
+
observer?.disconnect();
|
|
148
|
+
if ("anchorHidden" in panel.dataset) {
|
|
149
|
+
delete panel.dataset.anchorHidden;
|
|
150
|
+
panel.style.removeProperty("visibility");
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}, [open, target, anchorRef, panelRef, sideRequest, alignRequest, gapProperty, edgeProperty, flip, fitHeight, matchWidth]);
|
|
154
|
+
}
|
|
155
|
+
export {
|
|
156
|
+
fixedContainingBlock,
|
|
157
|
+
useAnchoredPosition
|
|
158
|
+
};
|
|
@@ -30,8 +30,15 @@ type FocusScopeOptions = {
|
|
|
30
30
|
active: boolean;
|
|
31
31
|
/** Wrap Tab / Shift+Tab within the scope. Default true. */
|
|
32
32
|
trap?: boolean;
|
|
33
|
-
/**
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Restore focus on deactivate. Default true: the previously focused
|
|
35
|
+
* element, or its nearest visible stand-in. A function names the target
|
|
36
|
+
* instead, read at the moment of closing, for a host that knows better
|
|
37
|
+
* than the opener: a table whose row deleted itself from this dialog
|
|
38
|
+
* sends focus to the next row rather than the table's first control. A
|
|
39
|
+
* function that returns null falls back to the default.
|
|
40
|
+
*/
|
|
41
|
+
restoreFocus?: boolean | (() => HTMLElement | null);
|
|
35
42
|
/**
|
|
36
43
|
* Where initial focus lands. Defaults to the first visible real control,
|
|
37
44
|
* then the first focusable of any kind, then the container itself.
|
|
@@ -40,5 +47,11 @@ type FocusScopeOptions = {
|
|
|
40
47
|
/** Make everything outside the scope inert while active. Default true. */
|
|
41
48
|
inertOutside?: boolean;
|
|
42
49
|
};
|
|
50
|
+
/**
|
|
51
|
+
* The panel of the topmost modal scope that isolates the page, or null when
|
|
52
|
+
* none is open. Keyboard shortcuts read it: while a modal is open, only the
|
|
53
|
+
* shortcuts that live inside it (or are global) may fire.
|
|
54
|
+
*/
|
|
55
|
+
export declare const activeModalPanel: () => HTMLElement | null;
|
|
43
56
|
export declare function useFocusScope(ref: React.RefObject<HTMLElement | null>, options: FocusScopeOptions): void;
|
|
44
57
|
export {};
|