@skewedaspect/sleekspace-ui 0.7.0 → 0.8.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/sleekspace-ui.css +1000 -307
- package/dist/sleekspace-ui.es.js +31559 -29868
- package/dist/sleekspace-ui.umd.js +32210 -30438
- package/dist/{components → src/components}/Accordion/SkAccordion.vue.d.ts +1 -1
- package/dist/{components → src/components}/Accordion/types.d.ts +1 -1
- package/dist/{components → src/components}/Alert/SkAlert.vue.d.ts +1 -1
- package/dist/{components → src/components}/Alert/types.d.ts +1 -1
- package/dist/{components → src/components}/Avatar/SkAvatar.vue.d.ts +1 -1
- package/dist/{components → src/components}/Avatar/types.d.ts +1 -1
- package/dist/{components → src/components}/Breadcrumbs/SkBreadcrumbs.vue.d.ts +2 -2
- package/dist/{components → src/components}/Breadcrumbs/types.d.ts +1 -1
- package/dist/{components → src/components}/Button/SkButton.vue.d.ts +9 -1
- package/dist/{components → src/components}/Button/types.d.ts +1 -1
- package/dist/{components → src/components}/Card/SkCard.vue.d.ts +1 -1
- package/dist/src/components/Card/types.d.ts +2 -0
- package/dist/{components → src/components}/Checkbox/SkCheckbox.vue.d.ts +1 -1
- package/dist/{components → src/components}/Checkbox/types.d.ts +1 -1
- package/dist/{components → src/components}/Collapsible/SkCollapsible.vue.d.ts +1 -1
- package/dist/src/components/Collapsible/types.d.ts +2 -0
- package/dist/{components → src/components}/ColorPicker/SkColorPicker.vue.d.ts +1 -1
- package/dist/{components → src/components}/ColorPicker/types.d.ts +1 -1
- package/dist/{components → src/components}/ContextMenu/SkContextMenu.vue.d.ts +1 -1
- package/dist/src/components/ContextMenu/types.d.ts +2 -0
- package/dist/{components → src/components}/Divider/SkDivider.vue.d.ts +1 -1
- package/dist/{components → src/components}/Dropdown/SkDropdown.vue.d.ts +1 -1
- package/dist/{components → src/components}/Dropdown/types.d.ts +1 -1
- package/dist/{components → src/components}/Input/SkInput.vue.d.ts +1 -1
- package/dist/{components → src/components}/Input/types.d.ts +1 -1
- package/dist/{components → src/components}/Listbox/SkListbox.vue.d.ts +1 -1
- package/dist/{components → src/components}/Listbox/types.d.ts +1 -1
- package/dist/{components → src/components}/Modal/SkModal.vue.d.ts +1 -1
- package/dist/{components → src/components}/Modal/types.d.ts +1 -1
- package/dist/{components → src/components}/NavBar/SkNavBar.vue.d.ts +2 -1
- package/dist/src/components/NavBar/context.d.ts +3 -0
- package/dist/{components → src/components}/NavBar/types.d.ts +1 -1
- package/dist/{components → src/components}/NumberInput/SkNumberInput.vue.d.ts +1 -1
- package/dist/{components → src/components}/NumberInput/types.d.ts +1 -1
- package/dist/src/components/Page/SkPage.vue.d.ts +161 -0
- package/dist/src/components/Page/SkPageSidebarToggle.vue.d.ts +41 -0
- package/dist/src/components/Page/index.d.ts +3 -0
- package/dist/src/components/Page/types.d.ts +39 -0
- package/dist/{components → src/components}/Pagination/SkPagination.vue.d.ts +3 -3
- package/dist/{components → src/components}/Pagination/types.d.ts +1 -1
- package/dist/{components → src/components}/Panel/SkPanel.vue.d.ts +1 -1
- package/dist/{components → src/components}/Panel/types.d.ts +1 -1
- package/dist/{components → src/components}/Popover/SkPopover.vue.d.ts +1 -1
- package/dist/{components → src/components}/Progress/SkProgress.vue.d.ts +1 -1
- package/dist/{components → src/components}/Progress/types.d.ts +1 -1
- package/dist/{components → src/components}/Radio/SkRadio.vue.d.ts +1 -1
- package/dist/{components → src/components}/Radio/types.d.ts +1 -1
- package/dist/{components → src/components}/ScrollArea/SkScrollArea.vue.d.ts +9 -0
- package/dist/{components → src/components}/ScrollArea/types.d.ts +1 -1
- package/dist/{components → src/components}/Select/SkSelect.vue.d.ts +1 -1
- package/dist/{components → src/components}/Select/SkSelectItem.vue.d.ts +6 -18
- package/dist/{components → src/components}/Select/types.d.ts +1 -1
- package/dist/{components → src/components}/Sidebar/SkSidebar.vue.d.ts +10 -2
- package/dist/{components → src/components}/Sidebar/types.d.ts +1 -1
- package/dist/{components → src/components}/Slider/SkSlider.vue.d.ts +1 -1
- package/dist/{components → src/components}/Slider/types.d.ts +1 -1
- package/dist/{components → src/components}/Spinner/SkSpinner.vue.d.ts +1 -1
- package/dist/{components → src/components}/Spinner/types.d.ts +1 -1
- package/dist/{components → src/components}/Splitter/types.d.ts +1 -1
- package/dist/{components → src/components}/Switch/SkSwitch.vue.d.ts +1 -1
- package/dist/{components → src/components}/Switch/types.d.ts +1 -1
- package/dist/{components → src/components}/Table/SkTable.vue.d.ts +1 -1
- package/dist/{components → src/components}/Table/types.d.ts +1 -1
- package/dist/{components → src/components}/Tabs/SkTab.vue.d.ts +1 -1
- package/dist/{components → src/components}/Tabs/SkTabs.vue.d.ts +2 -2
- package/dist/{components → src/components}/Tag/SkTag.vue.d.ts +1 -1
- package/dist/{components → src/components}/TagsInput/SkTagsInput.vue.d.ts +1 -1
- package/dist/{components → src/components}/TagsInput/types.d.ts +1 -1
- package/dist/{components → src/components}/Textarea/SkTextarea.vue.d.ts +1 -1
- package/dist/{components → src/components}/Textarea/types.d.ts +1 -1
- package/dist/{components → src/components}/Toolbar/types.d.ts +1 -1
- package/dist/{components → src/components}/Tooltip/SkTooltip.vue.d.ts +1 -1
- package/dist/{components → src/components}/Tooltip/types.d.ts +1 -1
- package/dist/{components → src/components}/TreeView/SkTreeView.vue.d.ts +5 -5
- package/dist/{components → src/components}/TreeView/types.d.ts +1 -1
- package/dist/src/composables/useFocusTrap.d.ts +17 -0
- package/dist/src/composables/usePageDrawer.d.ts +35 -0
- package/dist/src/composables/usePortalContext.test.d.ts +1 -0
- package/dist/{index.d.ts → src/index.d.ts} +2 -0
- package/dist/src/styles/mixins/fluidSize.test.d.ts +1 -0
- package/dist/tokens.css +60 -0
- package/llms-full.txt +6349 -0
- package/llms.txt +46 -0
- package/package.json +16 -11
- package/src/components/Button/SkButton.vue +25 -13
- package/src/components/NavBar/SkNavBar.vue +12 -1
- package/src/components/NavBar/context.ts +16 -0
- package/src/components/Page/SkPage.vue +460 -72
- package/src/components/Page/SkPageSidebarToggle.vue +148 -0
- package/src/components/Page/index.ts +1 -0
- package/src/components/Page/types.ts +30 -5
- package/src/components/ScrollArea/SkScrollArea.vue +12 -0
- package/src/components/Select/SkSelectItem.vue +2 -2
- package/src/components/Sidebar/SkSidebar.vue +10 -0
- package/src/components/TreeView/SkTreeView.vue +6 -6
- package/src/composables/useFocusTrap.test.ts +184 -0
- package/src/composables/useFocusTrap.ts +141 -0
- package/src/composables/usePageDrawer.ts +96 -0
- package/src/global.d.ts +1 -0
- package/src/index.ts +5 -0
- package/src/styles/components/_accordion.scss +15 -0
- package/src/styles/components/_alert.scss +1 -0
- package/src/styles/components/_avatar.scss +1 -0
- package/src/styles/components/_breadcrumbs.scss +7 -0
- package/src/styles/components/_button.scss +291 -214
- package/src/styles/components/_checkbox.scss +9 -1
- package/src/styles/components/_collapsible.scss +15 -0
- package/src/styles/components/_color-picker.scss +4 -1
- package/src/styles/components/_input.scss +1 -0
- package/src/styles/components/_listbox.scss +8 -2
- package/src/styles/components/_menu.scss +9 -2
- package/src/styles/components/_modal.scss +18 -2
- package/src/styles/components/_navbar.scss +22 -6
- package/src/styles/components/_number-input.scss +1 -0
- package/src/styles/components/_page.scss +220 -12
- package/src/styles/components/_pagination.scss +10 -1
- package/src/styles/components/_panel.scss +8 -3
- package/src/styles/components/_popover.scss +15 -2
- package/src/styles/components/_progress.scss +14 -0
- package/src/styles/components/_radio.scss +8 -1
- package/src/styles/components/_scroll-area.scss +56 -0
- package/src/styles/components/_select.scss +3 -1
- package/src/styles/components/_sidebar.scss +78 -38
- package/src/styles/components/_skeleton.scss +18 -0
- package/src/styles/components/_slider.scss +5 -4
- package/src/styles/components/_spinner.scss +15 -0
- package/src/styles/components/_switch.scss +5 -0
- package/src/styles/components/_table.scss +1 -0
- package/src/styles/components/_tabs.scss +6 -0
- package/src/styles/components/_tag.scss +2 -0
- package/src/styles/components/_tags-input.scss +1 -0
- package/src/styles/components/_textarea.scss +1 -0
- package/src/styles/components/_toast.scss +16 -1
- package/src/styles/components/_toolbar.scss +2 -0
- package/src/styles/components/_tooltip.scss +14 -1
- package/src/styles/components/_tree-view.scss +6 -1
- package/src/styles/mixins/_index.scss +1 -0
- package/src/styles/mixins/_responsive.scss +184 -0
- package/src/styles/mixins/fluidSize.test.ts +149 -0
- package/src/styles/tokens/_foundation-breakpoints.scss +26 -0
- package/src/styles/tokens/_foundation-z-index.scss +38 -0
- package/src/styles/tokens/index.scss +2 -0
- package/web-types.json +194 -14
- package/dist/components/Card/types.d.ts +0 -2
- package/dist/components/Collapsible/types.d.ts +0 -2
- package/dist/components/ContextMenu/types.d.ts +0 -2
- package/dist/components/Page/SkPage.vue.d.ts +0 -64
- package/dist/components/Page/index.d.ts +0 -2
- package/dist/components/Page/types.d.ts +0 -16
- package/dist/{components → src/components}/Accordion/SkAccordionItem.vue.d.ts +0 -0
- package/dist/{components → src/components}/Accordion/index.d.ts +0 -0
- package/dist/{components → src/components}/Avatar/index.d.ts +0 -0
- package/dist/{components → src/components}/Breadcrumbs/SkBreadcrumbItem.vue.d.ts +0 -0
- package/dist/{components → src/components}/Breadcrumbs/SkBreadcrumbSeparator.vue.d.ts +0 -0
- package/dist/{components → src/components}/Breadcrumbs/index.d.ts +0 -0
- package/dist/{components → src/components}/Checkbox/index.d.ts +0 -0
- package/dist/{components → src/components}/Collapsible/index.d.ts +0 -0
- package/dist/{components → src/components}/ColorPicker/index.d.ts +0 -0
- package/dist/{components → src/components}/ContextMenu/SkContextMenuCheckboxItem.vue.d.ts +0 -0
- package/dist/{components → src/components}/ContextMenu/SkContextMenuItem.vue.d.ts +0 -0
- package/dist/{components → src/components}/ContextMenu/SkContextMenuLabel.vue.d.ts +0 -0
- package/dist/{components → src/components}/ContextMenu/SkContextMenuRadioGroup.vue.d.ts +0 -0
- package/dist/{components → src/components}/ContextMenu/SkContextMenuRadioItem.vue.d.ts +0 -0
- package/dist/{components → src/components}/ContextMenu/SkContextMenuSeparator.vue.d.ts +0 -0
- package/dist/{components → src/components}/ContextMenu/SkContextMenuSubmenu.vue.d.ts +0 -0
- package/dist/{components → src/components}/ContextMenu/index.d.ts +0 -0
- package/dist/{components → src/components}/Divider/types.d.ts +0 -0
- package/dist/{components → src/components}/Dropdown/SkDropdownCheckboxItem.vue.d.ts +0 -0
- package/dist/{components → src/components}/Dropdown/SkDropdownMenuItem.vue.d.ts +0 -0
- package/dist/{components → src/components}/Dropdown/SkDropdownMenuLabel.vue.d.ts +0 -0
- package/dist/{components → src/components}/Dropdown/SkDropdownMenuSeparator.vue.d.ts +0 -0
- package/dist/{components → src/components}/Dropdown/SkDropdownRadioGroup.vue.d.ts +0 -0
- package/dist/{components → src/components}/Dropdown/SkDropdownRadioItem.vue.d.ts +0 -0
- package/dist/{components → src/components}/Dropdown/SkDropdownSubmenu.vue.d.ts +0 -0
- package/dist/{components → src/components}/Dropdown/index.d.ts +0 -0
- package/dist/{components → src/components}/Field/SkField.vue.d.ts +0 -0
- package/dist/{components → src/components}/Field/index.d.ts +0 -0
- package/dist/{components → src/components}/Field/types.d.ts +0 -0
- package/dist/{components → src/components}/Group/SkGroup.vue.d.ts +0 -0
- package/dist/{components → src/components}/Group/types.d.ts +0 -0
- package/dist/{components → src/components}/Input/index.d.ts +0 -0
- package/dist/{components → src/components}/Listbox/SkListboxItem.vue.d.ts +0 -0
- package/dist/{components → src/components}/Listbox/SkListboxSeparator.vue.d.ts +0 -0
- package/dist/{components → src/components}/Listbox/index.d.ts +0 -0
- package/dist/{components → src/components}/Modal/index.d.ts +0 -0
- package/dist/{components → src/components}/NavBar/index.d.ts +0 -0
- package/dist/{components → src/components}/NumberInput/index.d.ts +0 -0
- package/dist/{components → src/components}/Pagination/SkPaginationItem.vue.d.ts +0 -0
- package/dist/{components → src/components}/Pagination/index.d.ts +0 -0
- package/dist/{components → src/components}/Popover/index.d.ts +0 -0
- package/dist/{components → src/components}/Popover/types.d.ts +0 -0
- package/dist/{components → src/components}/Progress/index.d.ts +0 -0
- package/dist/{components → src/components}/Radio/SkRadioGroup.vue.d.ts +0 -0
- package/dist/{components → src/components}/Radio/index.d.ts +0 -0
- package/dist/{components → src/components}/ScrollArea/index.d.ts +0 -0
- package/dist/{components → src/components}/Select/SkSelectSeparator.vue.d.ts +0 -0
- package/dist/{components → src/components}/Select/index.d.ts +0 -0
- package/dist/{components → src/components}/Sidebar/SkSidebarItem.vue.d.ts +0 -0
- package/dist/{components → src/components}/Sidebar/SkSidebarSection.vue.d.ts +0 -0
- package/dist/{components → src/components}/Skeleton/SkSkeleton.vue.d.ts +2 -2
- /package/dist/{components → src/components}/Skeleton/index.d.ts +0 -0
- /package/dist/{components → src/components}/Skeleton/types.d.ts +0 -0
- /package/dist/{components → src/components}/Slider/index.d.ts +0 -0
- /package/dist/{components → src/components}/Spinner/index.d.ts +0 -0
- /package/dist/{components → src/components}/Splitter/SkSplitter.vue.d.ts +0 -0
- /package/dist/{components → src/components}/Splitter/SkSplitterHandle.vue.d.ts +0 -0
- /package/dist/{components → src/components}/Splitter/SkSplitterPanel.vue.d.ts +0 -0
- /package/dist/{components → src/components}/Splitter/index.d.ts +0 -0
- /package/dist/{components → src/components}/Switch/index.d.ts +0 -0
- /package/dist/{components → src/components}/Table/index.d.ts +0 -0
- /package/dist/{components → src/components}/Tabs/SkTabList.vue.d.ts +0 -0
- /package/dist/{components → src/components}/Tabs/SkTabPanel.vue.d.ts +0 -0
- /package/dist/{components → src/components}/Tabs/SkTabPanels.vue.d.ts +0 -0
- /package/dist/{components → src/components}/Tabs/types.d.ts +0 -0
- /package/dist/{components → src/components}/Tag/types.d.ts +0 -0
- /package/dist/{components → src/components}/TagsInput/index.d.ts +0 -0
- /package/dist/{components → src/components}/Textarea/index.d.ts +0 -0
- /package/dist/{components → src/components}/Theme/SkTheme.vue.d.ts +0 -0
- /package/dist/{components → src/components}/Theme/types.d.ts +0 -0
- /package/dist/{components → src/components}/Theme/useTheme.d.ts +0 -0
- /package/dist/{components → src/components}/Toast/SkToast.vue.d.ts +0 -0
- /package/dist/{components → src/components}/Toast/SkToastProvider.vue.d.ts +0 -0
- /package/dist/{components → src/components}/Toast/index.d.ts +0 -0
- /package/dist/{components → src/components}/Toast/types.d.ts +0 -0
- /package/dist/{components → src/components}/Toast/useToast.d.ts +0 -0
- /package/dist/{components → src/components}/Toolbar/SkToolbar.vue.d.ts +0 -0
- /package/dist/{components → src/components}/Toolbar/SkToolbarButton.vue.d.ts +0 -0
- /package/dist/{components → src/components}/Toolbar/SkToolbarSeparator.vue.d.ts +0 -0
- /package/dist/{components → src/components}/Toolbar/SkToolbarToggleGroup.vue.d.ts +0 -0
- /package/dist/{components → src/components}/Toolbar/SkToolbarToggleItem.vue.d.ts +0 -0
- /package/dist/{components → src/components}/Toolbar/index.d.ts +0 -0
- /package/dist/{components → src/components}/Tooltip/SkTooltipProvider.vue.d.ts +0 -0
- /package/dist/{components → src/components}/Tooltip/index.d.ts +0 -0
- /package/dist/{components → src/components}/TreeView/SkTreeItem.vue.d.ts +0 -0
- /package/dist/{components → src/components}/TreeView/index.d.ts +0 -0
- /package/dist/{composables → src/composables}/useCustomColors.d.ts +0 -0
- /package/dist/{composables → src/composables}/useCustomColors.test.d.ts +0 -0
- /package/dist/{composables/usePortalContext.test.d.ts → src/composables/useFocusTrap.test.d.ts} +0 -0
- /package/dist/{composables → src/composables}/usePortalContext.d.ts +0 -0
- /package/dist/{types.d.ts → src/types.d.ts} +0 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<!----------------------------------------------------------------------------------------------------------------------
|
|
2
|
+
- Page Sidebar Toggle Component
|
|
3
|
+
--------------------------------------------------------------------------------------------------------------------->
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<SkButton
|
|
7
|
+
v-if="drawer.isAvailable.value"
|
|
8
|
+
variant="ghost"
|
|
9
|
+
dense
|
|
10
|
+
:kind="effectiveKind"
|
|
11
|
+
:pressed="drawer.isOpen.value"
|
|
12
|
+
:aria-expanded="drawer.isOpen.value"
|
|
13
|
+
:aria-label="ariaLabel"
|
|
14
|
+
:class="classes"
|
|
15
|
+
@click="drawer.toggle"
|
|
16
|
+
>
|
|
17
|
+
<template #icon>
|
|
18
|
+
<slot>
|
|
19
|
+
<!-- Default glyphs: hamburger for the sidebar, vertical kebab for the aside. Override
|
|
20
|
+
via the default slot with any inline-sized element that inherits currentColor. -->
|
|
21
|
+
<svg
|
|
22
|
+
v-if="side === 'sidebar'"
|
|
23
|
+
viewBox="0 0 20 20"
|
|
24
|
+
width="20"
|
|
25
|
+
height="20"
|
|
26
|
+
aria-hidden="true"
|
|
27
|
+
focusable="false"
|
|
28
|
+
>
|
|
29
|
+
<path
|
|
30
|
+
d="M3 5h14M3 10h14M3 15h14"
|
|
31
|
+
stroke="currentColor"
|
|
32
|
+
stroke-width="2"
|
|
33
|
+
stroke-linecap="round"
|
|
34
|
+
fill="none"
|
|
35
|
+
/>
|
|
36
|
+
</svg>
|
|
37
|
+
<svg
|
|
38
|
+
v-else
|
|
39
|
+
viewBox="0 0 20 20"
|
|
40
|
+
width="20"
|
|
41
|
+
height="20"
|
|
42
|
+
aria-hidden="true"
|
|
43
|
+
focusable="false"
|
|
44
|
+
>
|
|
45
|
+
<circle cx="10" cy="4" r="1.6" fill="currentColor" />
|
|
46
|
+
<circle cx="10" cy="10" r="1.6" fill="currentColor" />
|
|
47
|
+
<circle cx="10" cy="16" r="1.6" fill="currentColor" />
|
|
48
|
+
</svg>
|
|
49
|
+
</slot>
|
|
50
|
+
</template>
|
|
51
|
+
</SkButton>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<!--------------------------------------------------------------------------------------------------------------------->
|
|
55
|
+
|
|
56
|
+
<script setup lang="ts">
|
|
57
|
+
/**
|
|
58
|
+
* @component SkPageSidebarToggle
|
|
59
|
+
* @description Button that toggles one of SkPage's drawers. Thin wrapper around SkButton -- inherits the
|
|
60
|
+
* design system's chrome and ghost-variant hover/pressed behavior. Auto-connects to the nearest SkPage
|
|
61
|
+
* via provide/inject, so no wiring is required; hides itself when the targeted panel is persistent.
|
|
62
|
+
* Use `side="sidebar"` (default) for the left drawer and `side="aside"` for the right. Default glyph is
|
|
63
|
+
* a hamburger for the sidebar and a vertical kebab for the aside so both toggles can live in the same
|
|
64
|
+
* navbar without visual collision; override via the default slot if you want something else.
|
|
65
|
+
*
|
|
66
|
+
* @example Twin toggles in a navbar
|
|
67
|
+
* ```vue
|
|
68
|
+
* <SkNavBar>
|
|
69
|
+
* <template #leading><SkPageSidebarToggle /></template>
|
|
70
|
+
* <template #brand>MyApp</template>
|
|
71
|
+
* <template #actions><SkPageSidebarToggle side="aside" /></template>
|
|
72
|
+
* </SkNavBar>
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @slot default - Custom glyph. Replaces the built-in SVG. Should be inline-sized and inherit
|
|
76
|
+
* `currentColor`.
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
import { computed, inject } from 'vue';
|
|
80
|
+
|
|
81
|
+
// Types
|
|
82
|
+
import type { SkButtonKind } from '../Button/types';
|
|
83
|
+
import type { SkPageDrawerSide } from './types';
|
|
84
|
+
|
|
85
|
+
// Components
|
|
86
|
+
import SkButton from '../Button/SkButton.vue';
|
|
87
|
+
|
|
88
|
+
// Composables
|
|
89
|
+
import { usePageDrawer } from '@/composables/usePageDrawer';
|
|
90
|
+
import { NAVBAR_KIND_KEY } from '../NavBar/context';
|
|
91
|
+
|
|
92
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
export interface SkPageSidebarToggleComponentProps
|
|
95
|
+
{
|
|
96
|
+
/**
|
|
97
|
+
* Which drawer this toggle controls. `sidebar` binds to the left drawer; `aside` binds to the right.
|
|
98
|
+
* @default 'sidebar'
|
|
99
|
+
*/
|
|
100
|
+
side ?: SkPageDrawerSide;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Override the toggle's color kind. By default the toggle inherits the enclosing SkNavBar's
|
|
104
|
+
* kind (when placed inside one) and falls back to `neutral` otherwise. Pass a kind explicitly
|
|
105
|
+
* to force a specific color scheme.
|
|
106
|
+
*/
|
|
107
|
+
kind ?: SkButtonKind;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Accessible label for the toggle. Screen readers announce this; sighted users see only the glyph.
|
|
111
|
+
* Defaults to a label matching `side`.
|
|
112
|
+
*/
|
|
113
|
+
ariaLabel ?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
const props = withDefaults(defineProps<SkPageSidebarToggleComponentProps>(), {
|
|
119
|
+
side: 'sidebar',
|
|
120
|
+
kind: undefined,
|
|
121
|
+
ariaLabel: undefined,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
const drawer = usePageDrawer(props.side);
|
|
127
|
+
const navbarKind = inject(NAVBAR_KIND_KEY, null);
|
|
128
|
+
|
|
129
|
+
const effectiveKind = computed<SkButtonKind>(() =>
|
|
130
|
+
{
|
|
131
|
+
if(props.kind) { return props.kind; }
|
|
132
|
+
if(navbarKind) { return navbarKind.value as SkButtonKind; }
|
|
133
|
+
return 'neutral';
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const ariaLabel = computed<string>(() =>
|
|
137
|
+
{
|
|
138
|
+
if(props.ariaLabel) { return props.ariaLabel; }
|
|
139
|
+
return props.side === 'aside' ? 'Toggle aside' : 'Toggle sidebar';
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const classes = computed(() => ({
|
|
143
|
+
'sk-page-sidebar-toggle': true,
|
|
144
|
+
[`sk-page-sidebar-toggle-${ props.side }`]: true,
|
|
145
|
+
}));
|
|
146
|
+
</script>
|
|
147
|
+
|
|
148
|
+
<!--------------------------------------------------------------------------------------------------------------------->
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
//----------------------------------------------------------------------------------------------------------------------
|
|
4
4
|
|
|
5
5
|
export { default as SkPage } from './SkPage.vue';
|
|
6
|
+
export { default as SkPageSidebarToggle } from './SkPageSidebarToggle.vue';
|
|
6
7
|
export * from './types';
|
|
7
8
|
|
|
8
9
|
//----------------------------------------------------------------------------------------------------------------------
|
|
@@ -6,22 +6,47 @@ import type { SkThemeName } from '../Theme/types';
|
|
|
6
6
|
|
|
7
7
|
//----------------------------------------------------------------------------------------------------------------------
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Sidebar / aside rendering mode.
|
|
11
|
+
* - `auto`: persistent above the matching breakpoint, drawer below. Default.
|
|
12
|
+
* - `persistent`: always inline, regardless of viewport size.
|
|
13
|
+
* - `drawer`: always off-canvas, opens over content.
|
|
14
|
+
*/
|
|
15
|
+
export type SkPagePanelMode = 'auto' | 'persistent' | 'drawer';
|
|
16
|
+
|
|
17
|
+
/** @deprecated Use `SkPagePanelMode`. Retained as an alias. */
|
|
18
|
+
export type SkPageSidebarMode = SkPagePanelMode;
|
|
19
|
+
|
|
20
|
+
/** Which drawer a toggle button controls. */
|
|
21
|
+
export type SkPageDrawerSide = 'sidebar' | 'aside';
|
|
11
22
|
|
|
12
23
|
/** Page props interface */
|
|
13
24
|
export interface SkPageProps
|
|
14
25
|
{
|
|
15
|
-
/** Sidebar position */
|
|
16
|
-
sidebarPosition ?: SkPageSidebarPosition;
|
|
17
26
|
/** Fixed header (stays at top when scrolling) */
|
|
18
27
|
fixedHeader ?: boolean;
|
|
19
28
|
/** Fixed footer (stays at bottom when scrolling) */
|
|
20
29
|
fixedFooter ?: boolean;
|
|
21
|
-
/** Custom sidebar width */
|
|
30
|
+
/** Custom sidebar width. CSS length. */
|
|
22
31
|
sidebarWidth ?: string;
|
|
32
|
+
/** Custom aside width. CSS length. */
|
|
33
|
+
asideWidth ?: string;
|
|
34
|
+
/** Sidebar rendering mode. Defaults to 'auto' (persistent above breakpoint, drawer below) */
|
|
35
|
+
sidebarMode ?: SkPagePanelMode;
|
|
36
|
+
/** Aside rendering mode. Defaults to 'auto'. */
|
|
37
|
+
asideMode ?: SkPagePanelMode;
|
|
38
|
+
/** Breakpoint below which sidebar `auto` mode switches to drawer. CSS length. Default: 1024px */
|
|
39
|
+
sidebarBreakpoint ?: string;
|
|
40
|
+
/** Breakpoint below which aside `auto` mode switches to drawer. CSS length. Default: 1024px */
|
|
41
|
+
asideBreakpoint ?: string;
|
|
42
|
+
/** Controlled sidebar drawer open state. When omitted, SkPage manages state internally */
|
|
43
|
+
sidebarOpen ?: boolean;
|
|
44
|
+
/** Controlled aside drawer open state. When omitted, SkPage manages state internally */
|
|
45
|
+
asideOpen ?: boolean;
|
|
23
46
|
/** Optional theme name — when provided, SkPage acts as a theme provider */
|
|
24
47
|
theme ?: SkThemeName;
|
|
48
|
+
/** Flush layout (no default gap/padding around main area or inside content) */
|
|
49
|
+
flush ?: boolean;
|
|
25
50
|
}
|
|
26
51
|
|
|
27
52
|
//----------------------------------------------------------------------------------------------------------------------
|
|
@@ -58,6 +58,15 @@
|
|
|
58
58
|
kind ?: SkScrollAreaKind;
|
|
59
59
|
baseColor ?: string;
|
|
60
60
|
textColor ?: string;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* When true, fade the scrollable edges with a CSS mask so content visibly tapers
|
|
64
|
+
* into/out of the viewport. Applies to whichever axis (or both) is scrollable.
|
|
65
|
+
* Override the fade distance via the `--sk-scroll-area-fade` custom property
|
|
66
|
+
* (default 1.5rem).
|
|
67
|
+
* @default false
|
|
68
|
+
*/
|
|
69
|
+
fade ?: boolean;
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
//------------------------------------------------------------------------------------------------------------------
|
|
@@ -68,6 +77,7 @@
|
|
|
68
77
|
kind: 'neutral',
|
|
69
78
|
baseColor: undefined,
|
|
70
79
|
textColor: undefined,
|
|
80
|
+
fade: false,
|
|
71
81
|
});
|
|
72
82
|
|
|
73
83
|
//------------------------------------------------------------------------------------------------------------------
|
|
@@ -81,6 +91,8 @@
|
|
|
81
91
|
const classes = computed(() => ({
|
|
82
92
|
'sk-scroll-area': true,
|
|
83
93
|
[`sk-${ props.kind }`]: true,
|
|
94
|
+
[`sk-${ props.orientation }`]: true,
|
|
95
|
+
'sk-fade': props.fade,
|
|
84
96
|
}));
|
|
85
97
|
</script>
|
|
86
98
|
|
|
@@ -82,8 +82,8 @@
|
|
|
82
82
|
//------------------------------------------------------------------------------------------------------------------
|
|
83
83
|
|
|
84
84
|
const textEl = useTemplateRef<InstanceType<typeof SelectItemText>>('textEl');
|
|
85
|
-
const register = inject<(value : string, label : string) => void>('sk-select-register', undefined);
|
|
86
|
-
const unregister = inject<(value : string) => void>('sk-select-unregister', undefined);
|
|
85
|
+
const register = inject<((value : string, label : string) => void) | undefined>('sk-select-register', undefined);
|
|
86
|
+
const unregister = inject<((value : string) => void) | undefined>('sk-select-unregister', undefined);
|
|
87
87
|
|
|
88
88
|
onMounted(() =>
|
|
89
89
|
{
|
|
@@ -86,6 +86,14 @@
|
|
|
86
86
|
* @default 'left'
|
|
87
87
|
*/
|
|
88
88
|
side ?: SkSidebarSide;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Opts out of the coarse-pointer touch-target floor on sidebar items (44px minimum on
|
|
92
|
+
* touch devices). Use in compact navigation contexts where density matters more than
|
|
93
|
+
* tap comfort. No effect on fine-pointer (mouse) devices.
|
|
94
|
+
* @default false
|
|
95
|
+
*/
|
|
96
|
+
dense ?: boolean;
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
//------------------------------------------------------------------------------------------------------------------
|
|
@@ -94,6 +102,7 @@
|
|
|
94
102
|
kind: 'neutral',
|
|
95
103
|
width: '180px',
|
|
96
104
|
side: 'left',
|
|
105
|
+
dense: false,
|
|
97
106
|
});
|
|
98
107
|
|
|
99
108
|
//------------------------------------------------------------------------------------------------------------------
|
|
@@ -114,6 +123,7 @@
|
|
|
114
123
|
'sk-sidebar': true,
|
|
115
124
|
[`sk-${ props.kind }`]: true,
|
|
116
125
|
'sk-sidebar-right': props.side === 'right',
|
|
126
|
+
'sk-dense': props.dense,
|
|
117
127
|
};
|
|
118
128
|
});
|
|
119
129
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
:default-expanded="expandedKeys"
|
|
13
13
|
:class="classes"
|
|
14
14
|
:style="customColorStyles"
|
|
15
|
-
@update:model-value="$emit('update:modelValue', $event)"
|
|
15
|
+
@update:model-value="$emit('update:modelValue', $event as T | T[])"
|
|
16
16
|
>
|
|
17
17
|
<template #default="{ flattenItems }">
|
|
18
18
|
<slot :flatten-items="flattenItems" />
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
|
|
41
41
|
//------------------------------------------------------------------------------------------------------------------
|
|
42
42
|
|
|
43
|
-
export interface SkTreeViewComponentProps
|
|
43
|
+
export interface SkTreeViewComponentProps<TItem extends Record<string, unknown> = Record<string, unknown>>
|
|
44
44
|
{
|
|
45
|
-
items :
|
|
46
|
-
getKey : (item :
|
|
47
|
-
modelValue ?:
|
|
45
|
+
items : TItem[];
|
|
46
|
+
getKey : (item : TItem) => string;
|
|
47
|
+
modelValue ?: TItem | TItem[];
|
|
48
48
|
multiple ?: boolean;
|
|
49
49
|
propagateSelect ?: boolean;
|
|
50
50
|
kind ?: SkTreeViewKind;
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
|
|
56
56
|
//------------------------------------------------------------------------------------------------------------------
|
|
57
57
|
|
|
58
|
-
const props = withDefaults(defineProps<SkTreeViewComponentProps
|
|
58
|
+
const props = withDefaults(defineProps<SkTreeViewComponentProps<T>>(), {
|
|
59
59
|
modelValue: undefined,
|
|
60
60
|
multiple: false,
|
|
61
61
|
propagateSelect: false,
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
2
|
+
// Focus Trap Composable Tests
|
|
3
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { defineComponent, h, nextTick, ref } from 'vue';
|
|
7
|
+
import { mount } from '@vue/test-utils';
|
|
8
|
+
import { useFocusTrap } from './useFocusTrap';
|
|
9
|
+
|
|
10
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
11
|
+
// Harness
|
|
12
|
+
//
|
|
13
|
+
// Renders a container with a few focusable elements plus an outside button. The active/container
|
|
14
|
+
// refs are exposed on the component instance so tests can flip the trap on and off.
|
|
15
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
function makeHarness(onEscape ?: () => void) : ReturnType<typeof defineComponent>
|
|
18
|
+
{
|
|
19
|
+
return defineComponent({
|
|
20
|
+
setup()
|
|
21
|
+
{
|
|
22
|
+
const active = ref<boolean>(false);
|
|
23
|
+
const container = ref<HTMLElement | null>(null);
|
|
24
|
+
useFocusTrap({ active, container, onEscape });
|
|
25
|
+
return { active, container };
|
|
26
|
+
},
|
|
27
|
+
render()
|
|
28
|
+
{
|
|
29
|
+
return h('div', [
|
|
30
|
+
h('button', { id: 'outside', type: 'button' }, 'outside'),
|
|
31
|
+
h(
|
|
32
|
+
'div',
|
|
33
|
+
{
|
|
34
|
+
ref: 'container',
|
|
35
|
+
tabindex: '-1',
|
|
36
|
+
},
|
|
37
|
+
[
|
|
38
|
+
h('button', { id: 'first', type: 'button' }, 'first'),
|
|
39
|
+
h('input', { id: 'middle', type: 'text' }),
|
|
40
|
+
h('button', { id: 'last', type: 'button' }, 'last'),
|
|
41
|
+
]
|
|
42
|
+
),
|
|
43
|
+
]);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
describe('useFocusTrap', () =>
|
|
51
|
+
{
|
|
52
|
+
let root : HTMLDivElement;
|
|
53
|
+
|
|
54
|
+
beforeEach(() =>
|
|
55
|
+
{
|
|
56
|
+
// happy-dom's elementFromPoint + layout are limited, so stub getClientRects to return
|
|
57
|
+
// a non-empty list on all our fixture elements (getFocusable filters on rects.length).
|
|
58
|
+
Element.prototype.getClientRects = vi.fn(() => ({
|
|
59
|
+
length: 1,
|
|
60
|
+
item: () => null,
|
|
61
|
+
*[Symbol.iterator]() { yield { width: 1, height: 1 } as DOMRect; },
|
|
62
|
+
} as unknown as DOMRectList));
|
|
63
|
+
|
|
64
|
+
root = document.createElement('div');
|
|
65
|
+
document.body.appendChild(root);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(() =>
|
|
69
|
+
{
|
|
70
|
+
document.body.removeChild(root);
|
|
71
|
+
vi.restoreAllMocks();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
//------------------------------------------------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
it('focuses the first focusable descendant when activated', async () =>
|
|
77
|
+
{
|
|
78
|
+
const wrapper = mount(makeHarness(), { attachTo: root });
|
|
79
|
+
wrapper.vm.active = true;
|
|
80
|
+
await nextTick();
|
|
81
|
+
// nextTick inside the watcher also runs an async nextTick before focusing; wait one more.
|
|
82
|
+
await nextTick();
|
|
83
|
+
|
|
84
|
+
const first = wrapper.find('#first').element as HTMLButtonElement;
|
|
85
|
+
expect(document.activeElement).toBe(first);
|
|
86
|
+
wrapper.unmount();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('restores focus to the previously focused element on deactivate', async () =>
|
|
90
|
+
{
|
|
91
|
+
const wrapper = mount(makeHarness(), { attachTo: root });
|
|
92
|
+
const outside = wrapper.find('#outside').element as HTMLButtonElement;
|
|
93
|
+
outside.focus();
|
|
94
|
+
|
|
95
|
+
wrapper.vm.active = true;
|
|
96
|
+
await nextTick();
|
|
97
|
+
await nextTick();
|
|
98
|
+
|
|
99
|
+
wrapper.vm.active = false;
|
|
100
|
+
await nextTick();
|
|
101
|
+
|
|
102
|
+
expect(document.activeElement).toBe(outside);
|
|
103
|
+
wrapper.unmount();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('tab from the last focusable cycles back to the first', async () =>
|
|
107
|
+
{
|
|
108
|
+
const wrapper = mount(makeHarness(), { attachTo: root });
|
|
109
|
+
wrapper.vm.active = true;
|
|
110
|
+
await nextTick();
|
|
111
|
+
await nextTick();
|
|
112
|
+
|
|
113
|
+
const last = wrapper.find('#last').element as HTMLButtonElement;
|
|
114
|
+
const first = wrapper.find('#first').element as HTMLButtonElement;
|
|
115
|
+
last.focus();
|
|
116
|
+
|
|
117
|
+
const event = new KeyboardEvent('keydown', { key: 'Tab', bubbles: true });
|
|
118
|
+
document.dispatchEvent(event);
|
|
119
|
+
expect(document.activeElement).toBe(first);
|
|
120
|
+
wrapper.unmount();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('shift-tab from the first focusable cycles to the last', async () =>
|
|
124
|
+
{
|
|
125
|
+
const wrapper = mount(makeHarness(), { attachTo: root });
|
|
126
|
+
wrapper.vm.active = true;
|
|
127
|
+
await nextTick();
|
|
128
|
+
await nextTick();
|
|
129
|
+
|
|
130
|
+
const first = wrapper.find('#first').element as HTMLButtonElement;
|
|
131
|
+
const last = wrapper.find('#last').element as HTMLButtonElement;
|
|
132
|
+
first.focus();
|
|
133
|
+
|
|
134
|
+
const event = new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true, bubbles: true });
|
|
135
|
+
document.dispatchEvent(event);
|
|
136
|
+
expect(document.activeElement).toBe(last);
|
|
137
|
+
wrapper.unmount();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('if focus escapes to an element outside the container, tab pulls it back to first', async () =>
|
|
141
|
+
{
|
|
142
|
+
const wrapper = mount(makeHarness(), { attachTo: root });
|
|
143
|
+
wrapper.vm.active = true;
|
|
144
|
+
await nextTick();
|
|
145
|
+
await nextTick();
|
|
146
|
+
|
|
147
|
+
const outside = wrapper.find('#outside').element as HTMLButtonElement;
|
|
148
|
+
const first = wrapper.find('#first').element as HTMLButtonElement;
|
|
149
|
+
// Simulate something stealing focus outside the trap (e.g. a scripted focus call).
|
|
150
|
+
outside.focus();
|
|
151
|
+
|
|
152
|
+
const event = new KeyboardEvent('keydown', { key: 'Tab', bubbles: true });
|
|
153
|
+
document.dispatchEvent(event);
|
|
154
|
+
expect(document.activeElement).toBe(first);
|
|
155
|
+
wrapper.unmount();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('calls onEscape when Escape is pressed while active', async () =>
|
|
159
|
+
{
|
|
160
|
+
const onEscape = vi.fn();
|
|
161
|
+
const wrapper = mount(makeHarness(onEscape), { attachTo: root });
|
|
162
|
+
wrapper.vm.active = true;
|
|
163
|
+
await nextTick();
|
|
164
|
+
await nextTick();
|
|
165
|
+
|
|
166
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
167
|
+
expect(onEscape).toHaveBeenCalledTimes(1);
|
|
168
|
+
wrapper.unmount();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('does not react to keypresses when inactive', async () =>
|
|
172
|
+
{
|
|
173
|
+
const onEscape = vi.fn();
|
|
174
|
+
const wrapper = mount(makeHarness(onEscape), { attachTo: root });
|
|
175
|
+
// Never activate.
|
|
176
|
+
|
|
177
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
178
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }));
|
|
179
|
+
expect(onEscape).not.toHaveBeenCalled();
|
|
180
|
+
wrapper.unmount();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
2
|
+
// Focus Trap Composable
|
|
3
|
+
//
|
|
4
|
+
// Minimal, dependency-free focus trap for modal/drawer patterns. Traps tab cycling inside a
|
|
5
|
+
// container while active, saves the previously-focused element on activation, and restores it
|
|
6
|
+
// on deactivation. Pair with your own ESC handler and overlay click-to-close as needed.
|
|
7
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
import { type Ref, nextTick, watch } from 'vue';
|
|
10
|
+
|
|
11
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
const FOCUSABLE_SELECTOR = [
|
|
14
|
+
'a[href]',
|
|
15
|
+
'button:not([disabled])',
|
|
16
|
+
'input:not([disabled]):not([type="hidden"])',
|
|
17
|
+
'select:not([disabled])',
|
|
18
|
+
'textarea:not([disabled])',
|
|
19
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
20
|
+
'audio[controls]',
|
|
21
|
+
'video[controls]',
|
|
22
|
+
'details > summary:first-of-type',
|
|
23
|
+
].join(',');
|
|
24
|
+
|
|
25
|
+
function getFocusable(container : HTMLElement) : HTMLElement[]
|
|
26
|
+
{
|
|
27
|
+
const nodes = container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR);
|
|
28
|
+
return Array.from(nodes).filter((el) =>
|
|
29
|
+
{
|
|
30
|
+
// Reject visually hidden or inert elements.
|
|
31
|
+
if(el.hasAttribute('inert')) { return false; }
|
|
32
|
+
if(el.getAttribute('aria-hidden') === 'true') { return false; }
|
|
33
|
+
const rects = el.getClientRects();
|
|
34
|
+
return rects.length > 0;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//----------------------------------------------------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
export interface UseFocusTrapOptions
|
|
41
|
+
{
|
|
42
|
+
/** When true, the trap is active: focus moves inside and tab cycling is constrained. */
|
|
43
|
+
active : Ref<boolean>;
|
|
44
|
+
|
|
45
|
+
/** Element whose descendants form the trap region. */
|
|
46
|
+
container : Ref<HTMLElement | null>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Optional callback fired when the user presses Escape. Most consumers will use this to
|
|
50
|
+
* close the modal/drawer.
|
|
51
|
+
*/
|
|
52
|
+
onEscape ?: () => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Trap focus inside a container while `active` is true. Restores focus to the previously
|
|
57
|
+
* active element when deactivated.
|
|
58
|
+
*/
|
|
59
|
+
export function useFocusTrap(options : UseFocusTrapOptions) : void
|
|
60
|
+
{
|
|
61
|
+
const { active, container, onEscape } = options;
|
|
62
|
+
|
|
63
|
+
let previouslyFocused : HTMLElement | null = null;
|
|
64
|
+
|
|
65
|
+
function handleKeydown(event : KeyboardEvent) : void
|
|
66
|
+
{
|
|
67
|
+
if(!active.value || !container.value) { return; }
|
|
68
|
+
|
|
69
|
+
if(event.key === 'Escape' && onEscape)
|
|
70
|
+
{
|
|
71
|
+
event.stopPropagation();
|
|
72
|
+
onEscape();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if(event.key !== 'Tab') { return; }
|
|
77
|
+
|
|
78
|
+
const focusable = getFocusable(container.value);
|
|
79
|
+
if(focusable.length === 0)
|
|
80
|
+
{
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const first = focusable[0];
|
|
86
|
+
const last = focusable[focusable.length - 1];
|
|
87
|
+
const activeEl = document.activeElement as HTMLElement | null;
|
|
88
|
+
|
|
89
|
+
if(event.shiftKey && (activeEl === first || !container.value.contains(activeEl)))
|
|
90
|
+
{
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
last.focus();
|
|
93
|
+
}
|
|
94
|
+
else if(!event.shiftKey && (activeEl === last || !container.value.contains(activeEl)))
|
|
95
|
+
{
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
first.focus();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
watch(active, async (isActive) =>
|
|
102
|
+
{
|
|
103
|
+
if(isActive)
|
|
104
|
+
{
|
|
105
|
+
previouslyFocused = document.activeElement as HTMLElement | null;
|
|
106
|
+
document.addEventListener('keydown', handleKeydown, true);
|
|
107
|
+
|
|
108
|
+
// Wait a tick for the container to mount, then focus the first focusable.
|
|
109
|
+
//
|
|
110
|
+
// `preventScroll: true` is load-bearing here: if the container is mid-transition and
|
|
111
|
+
// sits off-screen (e.g. a drawer whose enter-from transform parks it outside the
|
|
112
|
+
// viewport), the browser will otherwise scroll the nearest scrollable ancestor to
|
|
113
|
+
// bring the focused element into view -- yanking the entire page sideways while the
|
|
114
|
+
// drawer slides in.
|
|
115
|
+
await nextTick();
|
|
116
|
+
if(container.value)
|
|
117
|
+
{
|
|
118
|
+
const focusable = getFocusable(container.value);
|
|
119
|
+
if(focusable.length > 0)
|
|
120
|
+
{
|
|
121
|
+
focusable[0].focus({ preventScroll: true });
|
|
122
|
+
}
|
|
123
|
+
else
|
|
124
|
+
{
|
|
125
|
+
container.value.focus({ preventScroll: true });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else
|
|
130
|
+
{
|
|
131
|
+
document.removeEventListener('keydown', handleKeydown, true);
|
|
132
|
+
if(previouslyFocused && typeof previouslyFocused.focus === 'function')
|
|
133
|
+
{
|
|
134
|
+
previouslyFocused.focus({ preventScroll: true });
|
|
135
|
+
}
|
|
136
|
+
previouslyFocused = null;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//----------------------------------------------------------------------------------------------------------------------
|