@sveltier/ui 0.0.0-stage → 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +82 -2
- package/dist/components/Accordion/Accordion.svelte +74 -0
- package/dist/components/Accordion/Accordion.svelte.d.ts +25 -0
- package/dist/components/Accordion/accordion.css +88 -0
- package/dist/components/Accordion/index.d.ts +2 -0
- package/dist/components/Accordion/index.js +1 -0
- package/dist/components/Accordion/types.d.ts +64 -0
- package/dist/components/Accordion/types.js +1 -0
- package/dist/components/Alert/Alert.svelte +88 -0
- package/dist/components/Alert/Alert.svelte.d.ts +4 -0
- package/dist/components/Alert/alert.css +78 -0
- package/dist/components/Alert/index.d.ts +2 -0
- package/dist/components/Alert/index.js +1 -0
- package/dist/components/Alert/types.d.ts +67 -0
- package/dist/components/Alert/types.js +1 -0
- package/dist/components/Avatar/Avatar.svelte +47 -0
- package/dist/components/Avatar/Avatar.svelte.d.ts +4 -0
- package/dist/components/Avatar/avatar.css +34 -0
- package/dist/components/Avatar/index.d.ts +2 -0
- package/dist/components/Avatar/index.js +1 -0
- package/dist/components/Avatar/types.d.ts +47 -0
- package/dist/components/Avatar/types.js +1 -0
- package/dist/components/Badge/Badge.svelte +53 -0
- package/dist/components/Badge/Badge.svelte.d.ts +4 -0
- package/dist/components/Badge/badge.css +57 -0
- package/dist/components/Badge/index.d.ts +2 -0
- package/dist/components/Badge/index.js +1 -0
- package/dist/components/Badge/types.d.ts +43 -0
- package/dist/components/Badge/types.js +1 -0
- package/dist/components/Button/Button.svelte +85 -0
- package/dist/components/Button/Button.svelte.d.ts +4 -0
- package/dist/components/Button/button.css +132 -0
- package/dist/components/Button/index.d.ts +2 -0
- package/dist/components/Button/index.js +1 -0
- package/dist/components/Button/types.d.ts +76 -0
- package/dist/components/Button/types.js +1 -0
- package/dist/components/ButtonGroup/ButtonGroup.svelte +61 -0
- package/dist/components/ButtonGroup/ButtonGroup.svelte.d.ts +4 -0
- package/dist/components/ButtonGroup/button-group.css +39 -0
- package/dist/components/ButtonGroup/index.d.ts +2 -0
- package/dist/components/ButtonGroup/index.js +1 -0
- package/dist/components/ButtonGroup/types.d.ts +48 -0
- package/dist/components/ButtonGroup/types.js +1 -0
- package/dist/components/Card/Card.svelte +53 -0
- package/dist/components/Card/Card.svelte.d.ts +4 -0
- package/dist/components/Card/card.css +62 -0
- package/dist/components/Card/index.d.ts +2 -0
- package/dist/components/Card/index.js +1 -0
- package/dist/components/Card/types.d.ts +56 -0
- package/dist/components/Card/types.js +1 -0
- package/dist/components/Checkbox/Checkbox.svelte +105 -0
- package/dist/components/Checkbox/Checkbox.svelte.d.ts +4 -0
- package/dist/components/Checkbox/checkbox.css +138 -0
- package/dist/components/Checkbox/index.d.ts +2 -0
- package/dist/components/Checkbox/index.js +1 -0
- package/dist/components/Checkbox/types.d.ts +76 -0
- package/dist/components/Checkbox/types.js +1 -0
- package/dist/components/Chip/Chip.svelte +109 -0
- package/dist/components/Chip/Chip.svelte.d.ts +4 -0
- package/dist/components/Chip/chip.css +101 -0
- package/dist/components/Chip/index.d.ts +2 -0
- package/dist/components/Chip/index.js +1 -0
- package/dist/components/Chip/types.d.ts +69 -0
- package/dist/components/Chip/types.js +1 -0
- package/dist/components/Choice/Choice.svelte +60 -0
- package/dist/components/Choice/Choice.svelte.d.ts +4 -0
- package/dist/components/Choice/choice.css +75 -0
- package/dist/components/Choice/types.d.ts +43 -0
- package/dist/components/Choice/types.js +1 -0
- package/dist/components/Dialog/Dialog.svelte +36 -0
- package/dist/components/Dialog/Dialog.svelte.d.ts +4 -0
- package/dist/components/Dialog/index.d.ts +2 -0
- package/dist/components/Dialog/index.js +1 -0
- package/dist/components/Dialog/types.d.ts +50 -0
- package/dist/components/Dialog/types.js +1 -0
- package/dist/components/Divider/Divider.svelte +19 -0
- package/dist/components/Divider/Divider.svelte.d.ts +4 -0
- package/dist/components/Divider/divider.css +23 -0
- package/dist/components/Divider/index.d.ts +2 -0
- package/dist/components/Divider/index.js +1 -0
- package/dist/components/Divider/types.d.ts +10 -0
- package/dist/components/Divider/types.js +1 -0
- package/dist/components/Field/Field.svelte +95 -0
- package/dist/components/Field/Field.svelte.d.ts +4 -0
- package/dist/components/Field/field.css +210 -0
- package/dist/components/Field/index.d.ts +3 -0
- package/dist/components/Field/index.js +2 -0
- package/dist/components/Field/types.d.ts +109 -0
- package/dist/components/Field/types.js +2 -0
- package/dist/components/Form/Form.svelte +93 -0
- package/dist/components/Form/Form.svelte.d.ts +8 -0
- package/dist/components/Form/form.css +16 -0
- package/dist/components/Form/index.d.ts +2 -0
- package/dist/components/Form/index.js +1 -0
- package/dist/components/Form/types.d.ts +38 -0
- package/dist/components/Form/types.js +1 -0
- package/dist/components/Icon/Icon.svelte +38 -0
- package/dist/components/Icon/Icon.svelte.d.ts +4 -0
- package/dist/components/Icon/icon.css +33 -0
- package/dist/components/Icon/index.d.ts +2 -0
- package/dist/components/Icon/index.js +1 -0
- package/dist/components/Icon/types.d.ts +34 -0
- package/dist/components/Icon/types.js +1 -0
- package/dist/components/List/List.svelte +39 -0
- package/dist/components/List/List.svelte.d.ts +4 -0
- package/dist/components/List/ListItem.svelte +57 -0
- package/dist/components/List/ListItem.svelte.d.ts +4 -0
- package/dist/components/List/index.d.ts +3 -0
- package/dist/components/List/index.js +2 -0
- package/dist/components/List/list.css +127 -0
- package/dist/components/List/types.d.ts +67 -0
- package/dist/components/List/types.js +1 -0
- package/dist/components/Menu/Menu.svelte +195 -0
- package/dist/components/Menu/Menu.svelte.d.ts +4 -0
- package/dist/components/Menu/index.d.ts +2 -0
- package/dist/components/Menu/index.js +1 -0
- package/dist/components/Menu/menu.css +103 -0
- package/dist/components/Menu/types.d.ts +90 -0
- package/dist/components/Menu/types.js +1 -0
- package/dist/components/Modal/Modal.svelte +146 -0
- package/dist/components/Modal/Modal.svelte.d.ts +4 -0
- package/dist/components/Modal/modal.css +150 -0
- package/dist/components/Modal/types.d.ts +54 -0
- package/dist/components/Modal/types.js +1 -0
- package/dist/components/NativeSelect/NativeSelect.svelte +110 -0
- package/dist/components/NativeSelect/NativeSelect.svelte.d.ts +25 -0
- package/dist/components/NativeSelect/index.d.ts +2 -0
- package/dist/components/NativeSelect/index.js +1 -0
- package/dist/components/NativeSelect/native-select.css +45 -0
- package/dist/components/NativeSelect/types.d.ts +58 -0
- package/dist/components/NativeSelect/types.js +1 -0
- package/dist/components/NumberField/NumberField.svelte +246 -0
- package/dist/components/NumberField/NumberField.svelte.d.ts +4 -0
- package/dist/components/NumberField/index.d.ts +2 -0
- package/dist/components/NumberField/index.js +1 -0
- package/dist/components/NumberField/number-field.css +74 -0
- package/dist/components/NumberField/types.d.ts +73 -0
- package/dist/components/NumberField/types.js +1 -0
- package/dist/components/Popover/Popover.svelte +115 -0
- package/dist/components/Popover/Popover.svelte.d.ts +4 -0
- package/dist/components/Popover/index.d.ts +2 -0
- package/dist/components/Popover/index.js +1 -0
- package/dist/components/Popover/popover.css +67 -0
- package/dist/components/Popover/types.d.ts +83 -0
- package/dist/components/Popover/types.js +1 -0
- package/dist/components/Progress/ProgressCircular.svelte +61 -0
- package/dist/components/Progress/ProgressCircular.svelte.d.ts +4 -0
- package/dist/components/Progress/ProgressLinear.svelte +44 -0
- package/dist/components/Progress/ProgressLinear.svelte.d.ts +4 -0
- package/dist/components/Progress/index.d.ts +3 -0
- package/dist/components/Progress/index.js +2 -0
- package/dist/components/Progress/progress-circular.css +61 -0
- package/dist/components/Progress/progress-linear.css +67 -0
- package/dist/components/Progress/types.d.ts +53 -0
- package/dist/components/Progress/types.js +1 -0
- package/dist/components/RadioGroup/RadioGroup.svelte +149 -0
- package/dist/components/RadioGroup/RadioGroup.svelte.d.ts +25 -0
- package/dist/components/RadioGroup/index.d.ts +2 -0
- package/dist/components/RadioGroup/index.js +1 -0
- package/dist/components/RadioGroup/radio-group.css +139 -0
- package/dist/components/RadioGroup/types.d.ts +93 -0
- package/dist/components/RadioGroup/types.js +1 -0
- package/dist/components/SegmentedControl/SegmentedControl.svelte +70 -0
- package/dist/components/SegmentedControl/SegmentedControl.svelte.d.ts +25 -0
- package/dist/components/SegmentedControl/index.d.ts +2 -0
- package/dist/components/SegmentedControl/index.js +1 -0
- package/dist/components/SegmentedControl/segmented-control.css +68 -0
- package/dist/components/SegmentedControl/types.d.ts +60 -0
- package/dist/components/SegmentedControl/types.js +1 -0
- package/dist/components/Sheet/Sheet.svelte +56 -0
- package/dist/components/Sheet/Sheet.svelte.d.ts +4 -0
- package/dist/components/Sheet/index.d.ts +2 -0
- package/dist/components/Sheet/index.js +2 -0
- package/dist/components/Sheet/sheet.css +61 -0
- package/dist/components/Sheet/types.d.ts +51 -0
- package/dist/components/Sheet/types.js +1 -0
- package/dist/components/Skeleton/Skeleton.svelte +33 -0
- package/dist/components/Skeleton/Skeleton.svelte.d.ts +4 -0
- package/dist/components/Skeleton/index.d.ts +2 -0
- package/dist/components/Skeleton/index.js +1 -0
- package/dist/components/Skeleton/skeleton.css +70 -0
- package/dist/components/Skeleton/types.d.ts +25 -0
- package/dist/components/Skeleton/types.js +1 -0
- package/dist/components/Slider/Slider.svelte +153 -0
- package/dist/components/Slider/Slider.svelte.d.ts +4 -0
- package/dist/components/Slider/index.d.ts +2 -0
- package/dist/components/Slider/index.js +1 -0
- package/dist/components/Slider/slider.css +206 -0
- package/dist/components/Slider/types.d.ts +102 -0
- package/dist/components/Slider/types.js +1 -0
- package/dist/components/Switch/Switch.svelte +94 -0
- package/dist/components/Switch/Switch.svelte.d.ts +4 -0
- package/dist/components/Switch/index.d.ts +2 -0
- package/dist/components/Switch/index.js +1 -0
- package/dist/components/Switch/switch.css +118 -0
- package/dist/components/Switch/types.d.ts +67 -0
- package/dist/components/Switch/types.js +1 -0
- package/dist/components/Tabs/Tabs.svelte +93 -0
- package/dist/components/Tabs/Tabs.svelte.d.ts +25 -0
- package/dist/components/Tabs/index.d.ts +2 -0
- package/dist/components/Tabs/index.js +1 -0
- package/dist/components/Tabs/tabs.css +73 -0
- package/dist/components/Tabs/types.d.ts +59 -0
- package/dist/components/Tabs/types.js +1 -0
- package/dist/components/TextField/TextField.svelte +115 -0
- package/dist/components/TextField/TextField.svelte.d.ts +4 -0
- package/dist/components/TextField/index.d.ts +2 -0
- package/dist/components/TextField/index.js +1 -0
- package/dist/components/TextField/types.d.ts +57 -0
- package/dist/components/TextField/types.js +1 -0
- package/dist/components/Textarea/Textarea.svelte +116 -0
- package/dist/components/Textarea/Textarea.svelte.d.ts +4 -0
- package/dist/components/Textarea/index.d.ts +2 -0
- package/dist/components/Textarea/index.js +1 -0
- package/dist/components/Textarea/textarea.css +35 -0
- package/dist/components/Textarea/types.d.ts +60 -0
- package/dist/components/Textarea/types.js +1 -0
- package/dist/components/Tooltip/Tooltip.svelte +161 -0
- package/dist/components/Tooltip/Tooltip.svelte.d.ts +4 -0
- package/dist/components/Tooltip/index.d.ts +2 -0
- package/dist/components/Tooltip/index.js +1 -0
- package/dist/components/Tooltip/tooltip.css +44 -0
- package/dist/components/Tooltip/types.d.ts +55 -0
- package/dist/components/Tooltip/types.js +1 -0
- package/dist/core/defaults/DefaultsProvider.svelte +30 -0
- package/dist/core/defaults/DefaultsProvider.svelte.d.ts +13 -0
- package/dist/core/defaults/context.d.ts +45 -0
- package/dist/core/defaults/context.js +38 -0
- package/dist/core/display/display.d.ts +39 -0
- package/dist/core/display/display.js +53 -0
- package/dist/core/errors/codes.d.ts +124 -0
- package/dist/core/errors/codes.js +108 -0
- package/dist/core/errors/errors.d.ts +8 -0
- package/dist/core/errors/errors.js +7 -0
- package/dist/core/form/constraints.d.ts +15 -0
- package/dist/core/form/constraints.js +42 -0
- package/dist/core/form/context.d.ts +19 -0
- package/dist/core/form/context.js +4 -0
- package/dist/core/form/field.svelte.d.ts +34 -0
- package/dist/core/form/field.svelte.js +66 -0
- package/dist/core/form/number.d.ts +24 -0
- package/dist/core/form/number.js +71 -0
- package/dist/core/form/types.d.ts +22 -0
- package/dist/core/form/types.js +1 -0
- package/dist/core/locale/locale.d.ts +56 -0
- package/dist/core/locale/locale.js +82 -0
- package/dist/core/overlay/types.d.ts +33 -0
- package/dist/core/overlay/types.js +1 -0
- package/dist/core/props/classes.d.ts +21 -0
- package/dist/core/props/classes.js +44 -0
- package/dist/core/props/element.d.ts +18 -0
- package/dist/core/props/element.js +1 -0
- package/dist/core/props/types.d.ts +47 -0
- package/dist/core/props/types.js +16 -0
- package/dist/core/provider/UIProvider.svelte +63 -0
- package/dist/core/provider/UIProvider.svelte.d.ts +29 -0
- package/dist/core/theme/ThemeProvider.svelte +178 -0
- package/dist/core/theme/ThemeProvider.svelte.d.ts +25 -0
- package/dist/core/theme/color.d.ts +23 -0
- package/dist/core/theme/color.js +54 -0
- package/dist/core/theme/context.d.ts +25 -0
- package/dist/core/theme/context.js +14 -0
- package/dist/core/theme/css.d.ts +25 -0
- package/dist/core/theme/css.js +73 -0
- package/dist/core/theme/define.d.ts +11 -0
- package/dist/core/theme/define.js +21 -0
- package/dist/core/theme/resolve.d.ts +6 -0
- package/dist/core/theme/resolve.js +18 -0
- package/dist/core/theme/theme-provider.css +13 -0
- package/dist/core/theme/types.d.ts +35 -0
- package/dist/core/theme/types.js +61 -0
- package/dist/core/toast/ToastOutlet.svelte +192 -0
- package/dist/core/toast/ToastOutlet.svelte.d.ts +11 -0
- package/dist/core/toast/ToastProvider.svelte +39 -0
- package/dist/core/toast/ToastProvider.svelte.d.ts +19 -0
- package/dist/core/toast/toast.css +134 -0
- package/dist/core/toast/toast.svelte.d.ts +117 -0
- package/dist/core/toast/toast.svelte.js +133 -0
- package/dist/dev/off.d.ts +5 -0
- package/dist/dev/off.js +4 -0
- package/dist/dev/on.d.ts +20 -0
- package/dist/dev/on.js +49 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +83 -0
- package/dist/primitives/anchored/anchored.d.ts +39 -0
- package/dist/primitives/anchored/anchored.js +104 -0
- package/dist/primitives/anchored/fallback.d.ts +31 -0
- package/dist/primitives/anchored/fallback.js +73 -0
- package/dist/primitives/focus/focus-trap.d.ts +25 -0
- package/dist/primitives/focus/focus-trap.js +86 -0
- package/dist/primitives/focus/roving-focus.d.ts +22 -0
- package/dist/primitives/focus/roving-focus.js +86 -0
- package/dist/primitives/focus/tabbable.d.ts +9 -0
- package/dist/primitives/focus/tabbable.js +75 -0
- package/dist/primitives/inert/inert-others.d.ts +12 -0
- package/dist/primitives/inert/inert-others.js +44 -0
- package/dist/primitives/keyboard/keymap.d.ts +17 -0
- package/dist/primitives/keyboard/keymap.js +39 -0
- package/dist/primitives/keyboard/typeahead.d.ts +18 -0
- package/dist/primitives/keyboard/typeahead.js +50 -0
- package/dist/primitives/layer/dismissable.d.ts +32 -0
- package/dist/primitives/layer/dismissable.js +91 -0
- package/dist/primitives/motion/easing.d.ts +9 -0
- package/dist/primitives/motion/easing.js +46 -0
- package/dist/primitives/motion/motion.d.ts +59 -0
- package/dist/primitives/motion/motion.js +80 -0
- package/dist/primitives/portal/Portal.svelte +37 -0
- package/dist/primitives/portal/Portal.svelte.d.ts +9 -0
- package/dist/primitives/portal/PortalContent.svelte +15 -0
- package/dist/primitives/portal/PortalContent.svelte.d.ts +9 -0
- package/dist/primitives/portal/portal.css +8 -0
- package/dist/primitives/scroll-lock/scroll-lock.d.ts +10 -0
- package/dist/primitives/scroll-lock/scroll-lock.js +33 -0
- package/dist/primitives/virtual/virtual-list.svelte.d.ts +65 -0
- package/dist/primitives/virtual/virtual-list.svelte.js +291 -0
- package/dist/styles/base.css +44 -0
- package/dist/styles/props.css +140 -0
- package/dist/styles/tokens.css +84 -0
- package/dist/styles/visually-hidden.css +16 -0
- package/dist/themes/aurora.d.ts +13 -0
- package/dist/themes/aurora.js +103 -0
- package/dist/themes/heritage.d.ts +13 -0
- package/dist/themes/heritage.js +102 -0
- package/dist/themes/maison.d.ts +13 -0
- package/dist/themes/maison.js +99 -0
- package/dist/themes/milano.d.ts +13 -0
- package/dist/themes/milano.js +101 -0
- package/dist/themes/monolith.d.ts +13 -0
- package/dist/themes/monolith.js +101 -0
- package/package.json +87 -5
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export const SIDES = ['top', 'bottom', 'start', 'end'];
|
|
2
|
+
export const ALIGNS = ['start', 'center', 'end'];
|
|
3
|
+
export const PHYSICAL_SIDES = ['top', 'bottom', 'left', 'right'];
|
|
4
|
+
/**
|
|
5
|
+
* The physical side, and the alignment in physical terms (start is left or top), of a
|
|
6
|
+
* logical placement in the element's direction. Both placement paths use it, so they agree.
|
|
7
|
+
*/
|
|
8
|
+
export function physical(side, align, rtl) {
|
|
9
|
+
if (side === 'top' || side === 'bottom') {
|
|
10
|
+
const mirrored = align === 'start' ? 'end' : align === 'end' ? 'start' : align;
|
|
11
|
+
return [side, rtl ? mirrored : align];
|
|
12
|
+
}
|
|
13
|
+
return [(side === 'end') !== rtl ? 'right' : 'left', align];
|
|
14
|
+
}
|
|
15
|
+
/** `position-area` in physical keywords: `bottom span-right` is below, from the left edge. */
|
|
16
|
+
export function positionArea(side, align) {
|
|
17
|
+
if (align === 'center')
|
|
18
|
+
return side;
|
|
19
|
+
const vertical = side === 'top' || side === 'bottom';
|
|
20
|
+
const span = vertical
|
|
21
|
+
? align === 'start'
|
|
22
|
+
? 'right'
|
|
23
|
+
: 'left'
|
|
24
|
+
: align === 'start'
|
|
25
|
+
? 'bottom'
|
|
26
|
+
: 'top';
|
|
27
|
+
return `${side} span-${span}`;
|
|
28
|
+
}
|
|
29
|
+
// The margin that faces the anchor; flipping mirrors it to the other side.
|
|
30
|
+
const MARGIN = {
|
|
31
|
+
top: 'margin-bottom',
|
|
32
|
+
bottom: 'margin-top',
|
|
33
|
+
left: 'margin-right',
|
|
34
|
+
right: 'margin-left'
|
|
35
|
+
};
|
|
36
|
+
let names = 0;
|
|
37
|
+
/**
|
|
38
|
+
* Whether the element sits in a part of the page whose direction differs from the root's.
|
|
39
|
+
* Firefox (157) misplaces anchored elements there, so the script places them instead.
|
|
40
|
+
*/
|
|
41
|
+
function mixedDirection(element) {
|
|
42
|
+
const root = getComputedStyle(document.documentElement).direction;
|
|
43
|
+
return [element, element.parentElement].some((node) => node && getComputedStyle(node).direction !== root);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Places the element next to `anchor` and keeps it there on scroll and resize, flipping to
|
|
47
|
+
* the opposite side when there is not enough room. Uses CSS anchor positioning where the
|
|
48
|
+
* browser has it, otherwise loads a small script. The element must be positioned against the
|
|
49
|
+
* viewport: a popover in the top layer, or `position: fixed`.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* <button bind:this={trigger}>Account</button>
|
|
53
|
+
* <div popover="manual" {@attach anchored({ anchor: trigger, side: 'bottom', align: 'end' })}>…</div>
|
|
54
|
+
*/
|
|
55
|
+
export function anchored({ anchor, side = 'bottom', align = 'start', offset = 5, flip = true }) {
|
|
56
|
+
return (floating) => {
|
|
57
|
+
if (!anchor)
|
|
58
|
+
return;
|
|
59
|
+
const [physicalSide, physicalAlign] = physical(side, align, getComputedStyle(floating).direction === 'rtl');
|
|
60
|
+
if (!CSS.supports('position-try-fallbacks: flip-block') || mixedDirection(floating)) {
|
|
61
|
+
let stop;
|
|
62
|
+
let cancelled = false;
|
|
63
|
+
const placement = { side: physicalSide, align: physicalAlign, offset, flip };
|
|
64
|
+
void import('./fallback.js').then(({ follow }) => {
|
|
65
|
+
if (!cancelled)
|
|
66
|
+
stop = follow(anchor, floating, placement);
|
|
67
|
+
});
|
|
68
|
+
return () => {
|
|
69
|
+
cancelled = true;
|
|
70
|
+
stop?.();
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const name = `--sv-anchor-${++names}`;
|
|
74
|
+
const anchorStyle = anchor.style;
|
|
75
|
+
const existing = getComputedStyle(anchor).getPropertyValue('anchor-name');
|
|
76
|
+
anchorStyle.setProperty('anchor-name', existing && existing !== 'none' ? `${existing}, ${name}` : name);
|
|
77
|
+
const vertical = physicalSide === 'top' || physicalSide === 'bottom';
|
|
78
|
+
const style = floating.style;
|
|
79
|
+
const properties = {
|
|
80
|
+
'position-anchor': name,
|
|
81
|
+
'position-area': positionArea(physicalSide, physicalAlign),
|
|
82
|
+
'position-try-fallbacks': flip ? (vertical ? 'flip-block' : 'flip-inline') : 'none',
|
|
83
|
+
inset: 'auto',
|
|
84
|
+
margin: '0',
|
|
85
|
+
[MARGIN[physicalSide]]: `${offset}px`
|
|
86
|
+
};
|
|
87
|
+
for (const [property, value] of Object.entries(properties))
|
|
88
|
+
style.setProperty(property, value);
|
|
89
|
+
return () => {
|
|
90
|
+
for (const property of Object.keys(properties))
|
|
91
|
+
style.removeProperty(property);
|
|
92
|
+
// Other floating elements may share this anchor: remove only this name.
|
|
93
|
+
const rest = anchorStyle
|
|
94
|
+
.getPropertyValue('anchor-name')
|
|
95
|
+
.split(',')
|
|
96
|
+
.map((entry) => entry.trim())
|
|
97
|
+
.filter((entry) => entry && entry !== name);
|
|
98
|
+
if (rest.length)
|
|
99
|
+
anchorStyle.setProperty('anchor-name', rest.join(', '));
|
|
100
|
+
else
|
|
101
|
+
anchorStyle.removeProperty('anchor-name');
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Align, PhysicalSide as Physical } from './anchored.js';
|
|
2
|
+
interface Size {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
}
|
|
6
|
+
interface Rect extends Size {
|
|
7
|
+
top: number;
|
|
8
|
+
left: number;
|
|
9
|
+
bottom: number;
|
|
10
|
+
right: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Where the floating element goes: on `side` of the anchor, aligned along it, on the opposite
|
|
14
|
+
* side when that has more room (with `flip`), and kept inside the viewport.
|
|
15
|
+
*/
|
|
16
|
+
export declare function place(anchor: Rect, floating: Size, viewport: Size, side: Physical, align: Align, offset: number, flip?: boolean): {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
side: Physical;
|
|
20
|
+
};
|
|
21
|
+
export interface Placement {
|
|
22
|
+
/** Physical side, as anchored() resolves it from the writing direction. */
|
|
23
|
+
side: Physical;
|
|
24
|
+
/** Physical alignment: start is left or top. */
|
|
25
|
+
align: Align;
|
|
26
|
+
offset: number;
|
|
27
|
+
flip: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** Keeps `floating` placed against `anchor` until the returned function is called. */
|
|
30
|
+
export declare function follow(anchor: Element, floating: HTMLElement, { side, align, offset, flip }: Placement): () => void;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const OPPOSITE = {
|
|
2
|
+
top: 'bottom',
|
|
3
|
+
bottom: 'top',
|
|
4
|
+
left: 'right',
|
|
5
|
+
right: 'left'
|
|
6
|
+
};
|
|
7
|
+
// Distance kept from the viewport edges, in px.
|
|
8
|
+
const PADDING = 8;
|
|
9
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(value, max));
|
|
10
|
+
/**
|
|
11
|
+
* Where the floating element goes: on `side` of the anchor, aligned along it, on the opposite
|
|
12
|
+
* side when that has more room (with `flip`), and kept inside the viewport.
|
|
13
|
+
*/
|
|
14
|
+
export function place(anchor, floating, viewport, side, align, offset, flip = true) {
|
|
15
|
+
const room = {
|
|
16
|
+
top: anchor.top - PADDING,
|
|
17
|
+
bottom: viewport.height - anchor.bottom - PADDING,
|
|
18
|
+
left: anchor.left - PADDING,
|
|
19
|
+
right: viewport.width - anchor.right - PADDING
|
|
20
|
+
};
|
|
21
|
+
const vertical = side === 'top' || side === 'bottom';
|
|
22
|
+
const need = (vertical ? floating.height : floating.width) + offset;
|
|
23
|
+
if (flip && room[side] < need && room[OPPOSITE[side]] > room[side])
|
|
24
|
+
side = OPPOSITE[side];
|
|
25
|
+
const [start, length, size, axis] = vertical
|
|
26
|
+
? [anchor.left, anchor.width, floating.width, viewport.width]
|
|
27
|
+
: [anchor.top, anchor.height, floating.height, viewport.height];
|
|
28
|
+
const cross = clamp(align === 'start'
|
|
29
|
+
? start
|
|
30
|
+
: align === 'end'
|
|
31
|
+
? start + length - size
|
|
32
|
+
: start + (length - size) / 2, PADDING, axis - size - PADDING);
|
|
33
|
+
const main = side === 'top'
|
|
34
|
+
? anchor.top - offset - floating.height
|
|
35
|
+
: side === 'bottom'
|
|
36
|
+
? anchor.bottom + offset
|
|
37
|
+
: side === 'left'
|
|
38
|
+
? anchor.left - offset - floating.width
|
|
39
|
+
: anchor.right + offset;
|
|
40
|
+
return vertical ? { x: cross, y: main, side } : { x: main, y: cross, side };
|
|
41
|
+
}
|
|
42
|
+
/** Keeps `floating` placed against `anchor` until the returned function is called. */
|
|
43
|
+
export function follow(anchor, floating, { side, align, offset, flip }) {
|
|
44
|
+
const style = floating.style;
|
|
45
|
+
style.setProperty('inset', '0 auto auto 0');
|
|
46
|
+
style.setProperty('margin', '0');
|
|
47
|
+
let frame = 0;
|
|
48
|
+
function update() {
|
|
49
|
+
frame = 0;
|
|
50
|
+
const root = document.documentElement;
|
|
51
|
+
const result = place(anchor.getBoundingClientRect(), floating.getBoundingClientRect(), { width: root.clientWidth, height: root.clientHeight }, side, align, offset, flip);
|
|
52
|
+
style.setProperty('translate', `${result.x}px ${result.y}px`);
|
|
53
|
+
floating.dataset.side = result.side;
|
|
54
|
+
}
|
|
55
|
+
const schedule = () => {
|
|
56
|
+
frame ||= requestAnimationFrame(update);
|
|
57
|
+
};
|
|
58
|
+
const observer = new ResizeObserver(schedule);
|
|
59
|
+
observer.observe(anchor);
|
|
60
|
+
observer.observe(floating);
|
|
61
|
+
addEventListener('scroll', schedule, { capture: true, passive: true });
|
|
62
|
+
addEventListener('resize', schedule, { passive: true });
|
|
63
|
+
update();
|
|
64
|
+
return () => {
|
|
65
|
+
cancelAnimationFrame(frame);
|
|
66
|
+
observer.disconnect();
|
|
67
|
+
removeEventListener('scroll', schedule, { capture: true });
|
|
68
|
+
removeEventListener('resize', schedule);
|
|
69
|
+
for (const property of ['inset', 'margin', 'translate'])
|
|
70
|
+
style.removeProperty(property);
|
|
71
|
+
delete floating.dataset.side;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Attachment } from 'svelte/attachments';
|
|
2
|
+
import { type FocusableElement } from './tabbable.js';
|
|
3
|
+
export interface FocusTrapOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Where focus goes when the trap starts, unless it is already inside: an element, a selector
|
|
6
|
+
* inside the container, or `false` to leave it where it is. Defaults to the first tabbable
|
|
7
|
+
* element, else the container.
|
|
8
|
+
*/
|
|
9
|
+
initialFocus?: FocusableElement | string | false;
|
|
10
|
+
/**
|
|
11
|
+
* Where focus goes when the trap ends: `true` for the element that had it when the trap
|
|
12
|
+
* started, an element (pass the trigger: Safari does not focus a button on click, so the
|
|
13
|
+
* element that had focus may be the page itself), or `false` to leave it. Defaults to `true`.
|
|
14
|
+
*/
|
|
15
|
+
returnFocus?: boolean | FocusableElement | null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Keeps keyboard focus inside the element: Tab and Shift+Tab wrap at the edges, and focus
|
|
19
|
+
* that leaves by pointer or script is brought back. Traps stack: a trap inside another, or
|
|
20
|
+
* opened after it, takes over until it ends, then the previous one resumes.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* <div role="dialog" aria-modal="true" {@attach open && focusTrap()}>…</div>
|
|
24
|
+
*/
|
|
25
|
+
export declare function focusTrap({ initialFocus, returnFocus }?: FocusTrapOptions): Attachment<HTMLElement>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { tabbable } from './tabbable.js';
|
|
2
|
+
// Active traps, innermost last. Only the last one acts.
|
|
3
|
+
const traps = [];
|
|
4
|
+
/**
|
|
5
|
+
* Keeps keyboard focus inside the element: Tab and Shift+Tab wrap at the edges, and focus
|
|
6
|
+
* that leaves by pointer or script is brought back. Traps stack: a trap inside another, or
|
|
7
|
+
* opened after it, takes over until it ends, then the previous one resumes.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* <div role="dialog" aria-modal="true" {@attach open && focusTrap()}>…</div>
|
|
11
|
+
*/
|
|
12
|
+
export function focusTrap({ initialFocus, returnFocus = true } = {}) {
|
|
13
|
+
return (container) => {
|
|
14
|
+
const previous = returnFocus === true ? document.activeElement : returnFocus;
|
|
15
|
+
let madeFocusable = false;
|
|
16
|
+
let last;
|
|
17
|
+
function enter(target) {
|
|
18
|
+
if (target)
|
|
19
|
+
return target.focus();
|
|
20
|
+
if (!container.hasAttribute('tabindex')) {
|
|
21
|
+
container.tabIndex = -1;
|
|
22
|
+
madeFocusable = true;
|
|
23
|
+
}
|
|
24
|
+
container.focus();
|
|
25
|
+
}
|
|
26
|
+
const trap = {
|
|
27
|
+
container,
|
|
28
|
+
resume: () => enter(last?.isConnected && container.contains(last) ? last : tabbable(container)[0])
|
|
29
|
+
};
|
|
30
|
+
const active = () => traps.at(-1) === trap;
|
|
31
|
+
function onkeydown(event) {
|
|
32
|
+
if (event.key !== 'Tab' || event.defaultPrevented || !active())
|
|
33
|
+
return;
|
|
34
|
+
const items = tabbable(container);
|
|
35
|
+
const current = document.activeElement;
|
|
36
|
+
const edge = event.shiftKey ? items[0] : items.at(-1);
|
|
37
|
+
if (items.length === 0 || current === edge || !container.contains(current)) {
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
enter(event.shiftKey ? items.at(-1) : items[0]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function onfocusin(event) {
|
|
43
|
+
if (!active())
|
|
44
|
+
return;
|
|
45
|
+
const target = event.target;
|
|
46
|
+
if (container.contains(target))
|
|
47
|
+
last = target;
|
|
48
|
+
else
|
|
49
|
+
trap.resume();
|
|
50
|
+
}
|
|
51
|
+
// A trap mounted around active traps (children attach first) goes beneath them.
|
|
52
|
+
const inner = traps.findIndex((other) => container.contains(other.container));
|
|
53
|
+
if (inner < 0)
|
|
54
|
+
traps.push(trap);
|
|
55
|
+
else
|
|
56
|
+
traps.splice(inner, 0, trap);
|
|
57
|
+
document.addEventListener('keydown', onkeydown, true);
|
|
58
|
+
document.addEventListener('focusin', onfocusin, true);
|
|
59
|
+
if (initialFocus !== false && !container.contains(document.activeElement)) {
|
|
60
|
+
enter(typeof initialFocus === 'string'
|
|
61
|
+
? container.querySelector(initialFocus)
|
|
62
|
+
: (initialFocus ?? tabbable(container)[0]));
|
|
63
|
+
}
|
|
64
|
+
return () => {
|
|
65
|
+
document.removeEventListener('keydown', onkeydown, true);
|
|
66
|
+
document.removeEventListener('focusin', onfocusin, true);
|
|
67
|
+
traps.splice(traps.indexOf(trap), 1);
|
|
68
|
+
if (madeFocusable)
|
|
69
|
+
container.removeAttribute('tabindex');
|
|
70
|
+
// In a later task: after the other attachments of the closing surface have cleaned up
|
|
71
|
+
// (inertOthers may still hold the page inert now) and after the browser has moved
|
|
72
|
+
// focus for the press that closed it. Then give focus back if it is still ours
|
|
73
|
+
// (inside the trap, or lost to the page), and let the trap below take over again.
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
const focused = document.activeElement;
|
|
76
|
+
const lost = !focused || focused === document.body || container.contains(focused);
|
|
77
|
+
const restorable = previous instanceof HTMLElement || previous instanceof SVGElement;
|
|
78
|
+
if (lost && restorable && previous.isConnected)
|
|
79
|
+
previous.focus();
|
|
80
|
+
const below = traps.at(-1);
|
|
81
|
+
if (below && !below.container.contains(document.activeElement))
|
|
82
|
+
below.resume();
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Attachment } from 'svelte/attachments';
|
|
2
|
+
export interface RovingFocusOptions {
|
|
3
|
+
/** Which arrow keys move: left/right, up/down, or both. Defaults to `'horizontal'`. */
|
|
4
|
+
orientation?: 'horizontal' | 'vertical' | 'both';
|
|
5
|
+
/** Wrap from the last item to the first and back. Defaults to `true`. */
|
|
6
|
+
loop?: boolean;
|
|
7
|
+
/** Selector for the items, inside the container. Defaults to `[data-sv-item]`. */
|
|
8
|
+
items?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Gives a composite widget (toolbar, menu, radio group, tabs, listbox) one tab stop: the
|
|
12
|
+
* active item has `tabindex="0"`, the others `-1`. Arrow keys move along the orientation
|
|
13
|
+
* (left and right follow the writing direction), Home and End jump to the ends, and disabled
|
|
14
|
+
* items are skipped. The first stop is the item marked selected, checked or current.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* <div role="toolbar" {@attach rovingFocus()}>
|
|
18
|
+
* <button data-sv-item>Bold</button>
|
|
19
|
+
* <button data-sv-item>Italic</button>
|
|
20
|
+
* </div>
|
|
21
|
+
*/
|
|
22
|
+
export declare function rovingFocus({ orientation, loop, items }?: RovingFocusOptions): Attachment<HTMLElement>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const disabled = (item) => item.matches(':disabled') || item.getAttribute('aria-disabled') === 'true';
|
|
2
|
+
/**
|
|
3
|
+
* Gives a composite widget (toolbar, menu, radio group, tabs, listbox) one tab stop: the
|
|
4
|
+
* active item has `tabindex="0"`, the others `-1`. Arrow keys move along the orientation
|
|
5
|
+
* (left and right follow the writing direction), Home and End jump to the ends, and disabled
|
|
6
|
+
* items are skipped. The first stop is the item marked selected, checked or current.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* <div role="toolbar" {@attach rovingFocus()}>
|
|
10
|
+
* <button data-sv-item>Bold</button>
|
|
11
|
+
* <button data-sv-item>Italic</button>
|
|
12
|
+
* </div>
|
|
13
|
+
*/
|
|
14
|
+
export function rovingFocus({ orientation = 'horizontal', loop = true, items = '[data-sv-item]' } = {}) {
|
|
15
|
+
return (container) => {
|
|
16
|
+
const all = () => Array.from(container.querySelectorAll(items));
|
|
17
|
+
let current;
|
|
18
|
+
/** Makes `item` the tab stop, or keeps the current one, or picks the first enabled. */
|
|
19
|
+
function sync(item) {
|
|
20
|
+
const list = all();
|
|
21
|
+
const enabled = list.filter((candidate) => !disabled(candidate));
|
|
22
|
+
if (item)
|
|
23
|
+
current = item;
|
|
24
|
+
if (!current || !list.includes(current) || disabled(current)) {
|
|
25
|
+
current =
|
|
26
|
+
enabled.find((candidate) => candidate.matches('[aria-selected="true"],[aria-checked="true"],[aria-current]:not([aria-current="false"])')) ?? enabled[0];
|
|
27
|
+
}
|
|
28
|
+
for (const candidate of list)
|
|
29
|
+
candidate.tabIndex = candidate === current ? 0 : -1;
|
|
30
|
+
}
|
|
31
|
+
function onkeydown(event) {
|
|
32
|
+
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey)
|
|
33
|
+
return;
|
|
34
|
+
const list = all().filter((item) => !disabled(item));
|
|
35
|
+
const index = list.indexOf(event.target);
|
|
36
|
+
if (index < 0)
|
|
37
|
+
return;
|
|
38
|
+
const forward = getComputedStyle(container).direction === 'rtl' ? -1 : 1;
|
|
39
|
+
const horizontal = orientation !== 'vertical';
|
|
40
|
+
const vertical = orientation !== 'horizontal';
|
|
41
|
+
const step = {
|
|
42
|
+
ArrowRight: horizontal ? forward : undefined,
|
|
43
|
+
ArrowLeft: horizontal ? -forward : undefined,
|
|
44
|
+
ArrowDown: vertical ? 1 : undefined,
|
|
45
|
+
ArrowUp: vertical ? -1 : undefined
|
|
46
|
+
};
|
|
47
|
+
let next;
|
|
48
|
+
if (event.key === 'Home')
|
|
49
|
+
next = 0;
|
|
50
|
+
else if (event.key === 'End')
|
|
51
|
+
next = list.length - 1;
|
|
52
|
+
else if (step[event.key] !== undefined) {
|
|
53
|
+
next = index + step[event.key];
|
|
54
|
+
next = loop
|
|
55
|
+
? (next + list.length) % list.length
|
|
56
|
+
: Math.min(Math.max(next, 0), list.length - 1);
|
|
57
|
+
}
|
|
58
|
+
else
|
|
59
|
+
return;
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
sync(list[next]);
|
|
62
|
+
list[next]?.focus();
|
|
63
|
+
}
|
|
64
|
+
function onfocusin(event) {
|
|
65
|
+
const item = event.target.closest(items);
|
|
66
|
+
if (item && container.contains(item) && !disabled(item))
|
|
67
|
+
sync(item);
|
|
68
|
+
}
|
|
69
|
+
// Items come and go (filtering, async data): keep exactly one tab stop.
|
|
70
|
+
const observer = new MutationObserver(() => sync());
|
|
71
|
+
observer.observe(container, {
|
|
72
|
+
childList: true,
|
|
73
|
+
subtree: true,
|
|
74
|
+
attributes: true,
|
|
75
|
+
attributeFilter: ['disabled', 'aria-disabled']
|
|
76
|
+
});
|
|
77
|
+
container.addEventListener('keydown', onkeydown);
|
|
78
|
+
container.addEventListener('focusin', onfocusin);
|
|
79
|
+
sync();
|
|
80
|
+
return () => {
|
|
81
|
+
observer.disconnect();
|
|
82
|
+
container.removeEventListener('keydown', onkeydown);
|
|
83
|
+
container.removeEventListener('focusin', onfocusin);
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type FocusableElement = HTMLElement | SVGElement;
|
|
2
|
+
/** Whether `element` can be reached with Tab right now. */
|
|
3
|
+
export declare function isTabbable(element: Element): element is FocusableElement;
|
|
4
|
+
/**
|
|
5
|
+
* The elements inside `container` that Tab reaches, in the order Tab reaches them: positive
|
|
6
|
+
* `tabindex` first, then document order. Like browsers, a radio group contributes one stop,
|
|
7
|
+
* its checked radio or else its first.
|
|
8
|
+
*/
|
|
9
|
+
export declare function tabbable(container: ParentNode): FocusableElement[];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Elements that can be focusable at all; isTabbable() then checks the current state.
|
|
2
|
+
const CANDIDATES = [
|
|
3
|
+
'a[href]',
|
|
4
|
+
'area[href]',
|
|
5
|
+
'button',
|
|
6
|
+
'input:not([type="hidden"])',
|
|
7
|
+
'select',
|
|
8
|
+
'textarea',
|
|
9
|
+
'iframe',
|
|
10
|
+
'object',
|
|
11
|
+
'embed',
|
|
12
|
+
'audio[controls]',
|
|
13
|
+
'video[controls]',
|
|
14
|
+
'summary',
|
|
15
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
16
|
+
'[tabindex]'
|
|
17
|
+
].join(',');
|
|
18
|
+
/**
|
|
19
|
+
* The element's place in the Tab order. `tabIndex` reports -1 for editing hosts and media
|
|
20
|
+
* controls without a `tabindex` attribute, although Tab reaches them.
|
|
21
|
+
*/
|
|
22
|
+
function tabIndexOf(element) {
|
|
23
|
+
if (element.hasAttribute('tabindex'))
|
|
24
|
+
return element.tabIndex;
|
|
25
|
+
const reachable = element instanceof HTMLElement &&
|
|
26
|
+
(element.isContentEditable || element.matches('audio[controls],video[controls]'));
|
|
27
|
+
return reachable ? 0 : element.tabIndex;
|
|
28
|
+
}
|
|
29
|
+
/** Whether `element` can be reached with Tab right now. */
|
|
30
|
+
export function isTabbable(element) {
|
|
31
|
+
if (!(element instanceof HTMLElement || element instanceof SVGElement))
|
|
32
|
+
return false;
|
|
33
|
+
if (tabIndexOf(element) < 0 || element.matches(':disabled') || element.closest('[inert]')) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
// The content of a closed <details> is not rendered, except its own summary.
|
|
37
|
+
const details = element.closest('details:not([open])');
|
|
38
|
+
if (details && !(element.localName === 'summary' && element.parentElement === details)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return element.checkVisibility({ checkVisibilityCSS: true, visibilityProperty: true });
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The elements inside `container` that Tab reaches, in the order Tab reaches them: positive
|
|
45
|
+
* `tabindex` first, then document order. Like browsers, a radio group contributes one stop,
|
|
46
|
+
* its checked radio or else its first.
|
|
47
|
+
*/
|
|
48
|
+
export function tabbable(container) {
|
|
49
|
+
const found = [];
|
|
50
|
+
const radios = new Map();
|
|
51
|
+
for (const element of container.querySelectorAll(CANDIDATES)) {
|
|
52
|
+
if (!isTabbable(element))
|
|
53
|
+
continue;
|
|
54
|
+
if (element instanceof HTMLInputElement && element.type === 'radio' && element.name) {
|
|
55
|
+
const group = `${element.form?.id ?? ''}|${element.name}`;
|
|
56
|
+
const chosen = radios.get(group);
|
|
57
|
+
if (!chosen) {
|
|
58
|
+
radios.set(group, element);
|
|
59
|
+
found.push(element);
|
|
60
|
+
}
|
|
61
|
+
else if (element.checked && !chosen.checked) {
|
|
62
|
+
radios.set(group, element);
|
|
63
|
+
found[found.indexOf(chosen)] = element;
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
found.push(element);
|
|
68
|
+
}
|
|
69
|
+
// Stable sort: positive tabindex in ascending order, then everything else as it was.
|
|
70
|
+
const rank = (element) => {
|
|
71
|
+
const index = tabIndexOf(element);
|
|
72
|
+
return index > 0 ? index : Infinity;
|
|
73
|
+
};
|
|
74
|
+
return found.sort((a, b) => rank(a) - rank(b));
|
|
75
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Attachment } from 'svelte/attachments';
|
|
2
|
+
/**
|
|
3
|
+
* Makes everything outside the element inert (not focusable, not clickable, hidden from
|
|
4
|
+
* assistive technology) while attached, for modal surfaces that are not a `<dialog>`.
|
|
5
|
+
* Live regions stay active so announcements still reach screen readers. Calls stack: an
|
|
6
|
+
* inner modal never reactivates what an outer one made inert, and elements that were inert
|
|
7
|
+
* before stay inert.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* <div role="dialog" aria-modal="true" {@attach open && inertOthers()}>…</div>
|
|
11
|
+
*/
|
|
12
|
+
export declare function inertOthers(): Attachment<HTMLElement>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Elements this module made inert, with how many active calls need them inert.
|
|
2
|
+
const owned = new Map();
|
|
3
|
+
/**
|
|
4
|
+
* Makes everything outside the element inert (not focusable, not clickable, hidden from
|
|
5
|
+
* assistive technology) while attached, for modal surfaces that are not a `<dialog>`.
|
|
6
|
+
* Live regions stay active so announcements still reach screen readers. Calls stack: an
|
|
7
|
+
* inner modal never reactivates what an outer one made inert, and elements that were inert
|
|
8
|
+
* before stay inert.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* <div role="dialog" aria-modal="true" {@attach open && inertOthers()}>…</div>
|
|
12
|
+
*/
|
|
13
|
+
export function inertOthers() {
|
|
14
|
+
return (element) => {
|
|
15
|
+
const changed = [];
|
|
16
|
+
for (let node = element; node.parentElement && node !== document.body; node = node.parentElement) {
|
|
17
|
+
for (const sibling of node.parentElement.children) {
|
|
18
|
+
if (sibling === node || sibling.matches('[aria-live]'))
|
|
19
|
+
continue;
|
|
20
|
+
const count = owned.get(sibling);
|
|
21
|
+
if (count !== undefined)
|
|
22
|
+
owned.set(sibling, count + 1);
|
|
23
|
+
else if (sibling.hasAttribute('inert'))
|
|
24
|
+
continue;
|
|
25
|
+
else {
|
|
26
|
+
owned.set(sibling, 1);
|
|
27
|
+
sibling.setAttribute('inert', '');
|
|
28
|
+
}
|
|
29
|
+
changed.push(sibling);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return () => {
|
|
33
|
+
for (const sibling of changed) {
|
|
34
|
+
const count = (owned.get(sibling) ?? 1) - 1;
|
|
35
|
+
if (count > 0)
|
|
36
|
+
owned.set(sibling, count);
|
|
37
|
+
else {
|
|
38
|
+
owned.delete(sibling);
|
|
39
|
+
sibling.removeAttribute('inert');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Attachment } from 'svelte/attachments';
|
|
2
|
+
/** Runs for a key combination. Return `false` to let the event through unhandled. */
|
|
3
|
+
export type KeyHandler = (event: KeyboardEvent) => void | false;
|
|
4
|
+
/**
|
|
5
|
+
* The combination of an event as `keymap` names it: `Ctrl+Alt+Shift+Meta+Key`. With `rtl`
|
|
6
|
+
* given, horizontal arrows become `ArrowForward` / `ArrowBackward` for that direction.
|
|
7
|
+
*/
|
|
8
|
+
export declare function combination(event: KeyboardEvent, rtl?: boolean): string;
|
|
9
|
+
/**
|
|
10
|
+
* Binds keys to handlers on the element. A handled key has its default prevented. Write
|
|
11
|
+
* combinations as `Ctrl+Alt+Shift+Meta+Key` with `event.key` names; `ArrowForward` and
|
|
12
|
+
* `ArrowBackward` follow the writing direction, and physical arrow names work too.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* <div role="slider" {@attach keymap({ ArrowForward: up, ArrowBackward: down, Home: min, End: max })}>
|
|
16
|
+
*/
|
|
17
|
+
export declare function keymap(map: Record<string, KeyHandler>): Attachment<HTMLElement>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The combination of an event as `keymap` names it: `Ctrl+Alt+Shift+Meta+Key`. With `rtl`
|
|
3
|
+
* given, horizontal arrows become `ArrowForward` / `ArrowBackward` for that direction.
|
|
4
|
+
*/
|
|
5
|
+
export function combination(event, rtl) {
|
|
6
|
+
let key = event.key;
|
|
7
|
+
if (rtl !== undefined && (key === 'ArrowRight' || key === 'ArrowLeft')) {
|
|
8
|
+
key = (key === 'ArrowRight') !== rtl ? 'ArrowForward' : 'ArrowBackward';
|
|
9
|
+
}
|
|
10
|
+
// event.key already carries Shift for characters: "?" rather than "Shift+/".
|
|
11
|
+
const shift = event.shiftKey && key.length > 1;
|
|
12
|
+
return ((event.ctrlKey ? 'Ctrl+' : '') +
|
|
13
|
+
(event.altKey ? 'Alt+' : '') +
|
|
14
|
+
(shift ? 'Shift+' : '') +
|
|
15
|
+
(event.metaKey ? 'Meta+' : '') +
|
|
16
|
+
key);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Binds keys to handlers on the element. A handled key has its default prevented. Write
|
|
20
|
+
* combinations as `Ctrl+Alt+Shift+Meta+Key` with `event.key` names; `ArrowForward` and
|
|
21
|
+
* `ArrowBackward` follow the writing direction, and physical arrow names work too.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* <div role="slider" {@attach keymap({ ArrowForward: up, ArrowBackward: down, Home: min, End: max })}>
|
|
25
|
+
*/
|
|
26
|
+
export function keymap(map) {
|
|
27
|
+
return (element) => {
|
|
28
|
+
function onkeydown(event) {
|
|
29
|
+
if (event.defaultPrevented)
|
|
30
|
+
return;
|
|
31
|
+
const rtl = getComputedStyle(element).direction === 'rtl';
|
|
32
|
+
const handler = map[combination(event, rtl)] ?? map[combination(event)];
|
|
33
|
+
if (handler && handler(event) !== false)
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
}
|
|
36
|
+
element.addEventListener('keydown', onkeydown);
|
|
37
|
+
return () => element.removeEventListener('keydown', onkeydown);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Attachment } from 'svelte/attachments';
|
|
2
|
+
export interface TypeaheadOptions {
|
|
3
|
+
/** Selector for the items, inside the container. Defaults to `[data-sv-item]`. */
|
|
4
|
+
items?: string;
|
|
5
|
+
/** How long typed characters keep building one search, in ms. Defaults to 500. */
|
|
6
|
+
timeout?: number;
|
|
7
|
+
/** What a match does. Defaults to focusing the item. */
|
|
8
|
+
onMatch?: (item: HTMLElement) => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Typing moves focus to the next item whose label starts with what was typed, as menus,
|
|
12
|
+
* listboxes and trees do. Repeating one character cycles through the items starting with it.
|
|
13
|
+
* Labels come from `data-sv-label`, `aria-label`, then the text.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* <ul role="listbox" {@attach rovingFocus({ orientation: 'vertical' })} {@attach typeahead()}>
|
|
17
|
+
*/
|
|
18
|
+
export declare function typeahead({ items, timeout, onMatch }?: TypeaheadOptions): Attachment<HTMLElement>;
|