sheer-ui 0.4.0 → 0.4.3
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/package.json +1 -1
- package/src/lib/components/accordion/components/accordion-item.svelte +3 -3
- package/src/lib/components/accordion/components/accordion.svelte +29 -13
- package/src/lib/components/accordion/types.ts +3 -3
- package/src/lib/components/avatar/avatar.svelte.ts +34 -37
- package/src/lib/components/avatar/components/avatar-fallback.svelte +9 -6
- package/src/lib/components/avatar/components/avatar-image.svelte +18 -9
- package/src/lib/components/avatar/components/avatar.svelte +20 -16
- package/src/lib/components/checkbox/checkbox.svelte.ts +45 -55
- package/src/lib/components/checkbox/components/checkbox-group-label.svelte +9 -6
- package/src/lib/components/checkbox/components/checkbox-group.svelte +25 -14
- package/src/lib/components/checkbox/components/checkbox.svelte +39 -21
- package/src/lib/components/combobox/components/combobox.svelte +2 -2
- package/src/lib/components/combobox/select/components/select.svelte +2 -2
- package/src/lib/components/context-menu/components/context-menu-trigger.svelte +1 -1
- package/src/lib/components/date-field/date-field.svelte.ts +2 -3
- package/src/lib/components/dialog/components/dialog-close.svelte +13 -8
- package/src/lib/components/dialog/components/dialog-content-headless.svelte +14 -11
- package/src/lib/components/dialog/components/dialog-description.svelte +9 -6
- package/src/lib/components/dialog/components/dialog-overlay.svelte +10 -7
- package/src/lib/components/dialog/components/dialog-title.svelte +12 -7
- package/src/lib/components/dialog/components/dialog-trigger.svelte +12 -7
- package/src/lib/components/dialog/components/dialog.svelte +10 -4
- package/src/lib/components/dialog/components/modal-surface.svelte +11 -8
- package/src/lib/components/dialog/dialog.svelte.ts +73 -61
- package/src/lib/components/drawer/drawer.svelte +8 -3
- package/src/lib/components/link-preview/components/link-preview-content.svelte +15 -8
- package/src/lib/components/link-preview/components/link-preview-trigger.svelte +9 -6
- package/src/lib/components/link-preview/components/link-preview.svelte +19 -11
- package/src/lib/components/link-preview/link-preview.svelte.ts +42 -43
- package/src/lib/components/menu/components/menu-checkbox-group.svelte +6 -4
- package/src/lib/components/menu/components/menu-checkbox-item.svelte +11 -7
- package/src/lib/components/menu/menu.svelte.ts +29 -19
- package/src/lib/components/menubar/menubar.svelte.ts +7 -6
- package/src/lib/components/meter/components/meter.svelte +18 -9
- package/src/lib/components/meter/meter.svelte.ts +15 -18
- package/src/lib/components/navigation-menu/components/navigation-menu-content.svelte +1 -1
- package/src/lib/components/navigation-menu/components/navigation-menu-indicator.svelte +2 -2
- package/src/lib/components/navigation-menu/components/navigation-menu-trigger.svelte +1 -1
- package/src/lib/components/navigation-menu/navigation-menu.svelte.ts +57 -60
- package/src/lib/components/pagination/components/pagination-next-button.svelte +12 -7
- package/src/lib/components/pagination/components/pagination-page.svelte +15 -8
- package/src/lib/components/pagination/components/pagination-prev-button.svelte +12 -7
- package/src/lib/components/pagination/components/pagination.svelte +31 -18
- package/src/lib/components/pagination/pagination.svelte.ts +48 -64
- package/src/lib/components/pin-input/pin-input.svelte.ts +7 -8
- package/src/lib/components/pin-input/usePasswordManager.svelte.ts +2 -2
- package/src/lib/components/popover/components/popover-close.svelte +9 -6
- package/src/lib/components/popover/components/popover-content.svelte +12 -7
- package/src/lib/components/popover/components/popover-trigger.svelte +21 -10
- package/src/lib/components/popover/components/popover.svelte +9 -8
- package/src/lib/components/popover/popover.svelte.ts +68 -74
- package/src/lib/components/progress/components/progress.svelte +18 -9
- package/src/lib/components/progress/progress.svelte.ts +14 -17
- package/src/lib/components/scroll-area/scroll-area.svelte.ts +9 -10
- package/src/lib/components/separator/components/separator.svelte +15 -8
- package/src/lib/components/separator/separator.svelte.ts +12 -15
- package/src/lib/components/sidebar/sidebar-mobile-surface.svelte +8 -4
- package/src/lib/components/slider/components/slider.svelte +2 -1
- package/src/lib/components/tabs/components/tabs-content.svelte +12 -7
- package/src/lib/components/tabs/components/tabs-list.svelte +9 -6
- package/src/lib/components/tabs/components/tabs-trigger.svelte +15 -8
- package/src/lib/components/tabs/components/tabs.svelte +28 -17
- package/src/lib/components/tabs/tabs.svelte.ts +51 -67
- package/src/lib/components/time-field/time-field.svelte.ts +2 -2
- package/src/lib/components/toggle/components/toggle.svelte +19 -14
- package/src/lib/components/toggle/toggle.svelte.ts +15 -20
- package/src/lib/components/toggle-group/components/toggle-group-item.svelte +15 -8
- package/src/lib/components/toggle-group/components/toggle-group.svelte +28 -16
- package/src/lib/components/toggle-group/toggle-group.svelte.ts +33 -35
- package/src/lib/components/toolbar/components/toolbar-group-item.svelte +15 -8
- package/src/lib/components/toolbar/components/toolbar-group.svelte +19 -14
- package/src/lib/components/toolbar/toolbar.svelte.ts +28 -30
- package/src/lib/components/tooltip/components/tooltip-content.svelte +15 -8
- package/src/lib/components/tooltip/components/tooltip-provider.svelte +18 -7
- package/src/lib/components/tooltip/components/tooltip-trigger.svelte +21 -10
- package/src/lib/components/tooltip/components/tooltip.svelte +34 -21
- package/src/lib/components/tooltip/tooltip.svelte.ts +101 -125
- package/src/lib/internal/dismissible-layer/use-dismissable-layer.svelte.ts +3 -4
- package/src/lib/internal/escape-layer/use-escape-layer.svelte.ts +13 -29
- package/src/lib/internal/floating-layer/use-floating-layer.svelte.ts +26 -30
- package/src/lib/internal/group-value.svelte.ts +11 -10
- package/src/lib/internal/native-popover.svelte.ts +6 -6
- package/src/lib/internal/{roving-focus-group.ts → roving-focus-group.svelte.ts} +17 -19
- package/src/lib/internal/roving-focus-item.svelte.ts +12 -17
- package/src/lib/internal/selection.svelte.ts +55 -41
- package/src/lib/internal/{state-machine.ts → state-machine.svelte.ts} +6 -8
- package/src/lib/internal/timeout-fn.svelte.ts +6 -4
- package/src/lib/internal/tools/box.svelte.ts +5 -114
- package/src/lib/internal/tools/index.ts +1 -11
- package/src/lib/internal/tools/types.ts +0 -3
- package/src/lib/internal/typeahead.svelte.ts +9 -13
- package/src/lib/internal/types.ts +6 -0
- package/src/lib/internal/vaul/use-drawer-content.svelte.ts +2 -2
- package/src/lib/internal/vaul/use-drawer-root.svelte.ts +12 -7
- package/src/lib/components/accordion/accordion.svelte.ts +0 -43
|
@@ -5,10 +5,7 @@ import {
|
|
|
5
5
|
getWindow,
|
|
6
6
|
styleToString,
|
|
7
7
|
type ReadableBoxedValues,
|
|
8
|
-
type ReadableBox,
|
|
9
8
|
type RefAttachment,
|
|
10
|
-
simpleBox,
|
|
11
|
-
boxWith,
|
|
12
9
|
} from '../tools/index.js';
|
|
13
10
|
import type { WithRefOpts } from '../types.js';
|
|
14
11
|
import type { Direction, StyleProperties } from '../index.js';
|
|
@@ -60,10 +57,16 @@ export class FloatingRootState {
|
|
|
60
57
|
return tooltip ? setFloatingTooltipRoot(root) : setFloatingRoot(root);
|
|
61
58
|
}
|
|
62
59
|
readonly anchorName: string;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
// Getters over whoever owns the anchor (trigger attachment, customAnchor, pointer rect): a derived reading through them tracks the owner.
|
|
61
|
+
#customAnchorSource: () => CustomAnchorNode = $state.raw(() => null);
|
|
62
|
+
triggerSource: () => AnchorNode = $state.raw(() => null);
|
|
63
|
+
|
|
64
|
+
constructor(anchorName: string) {
|
|
65
|
+
this.anchorName = anchorName;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get anchorNode(): AnchorNode {
|
|
69
|
+
const customAnchor = this.#customAnchorSource();
|
|
67
70
|
|
|
68
71
|
if (customAnchor) {
|
|
69
72
|
if (typeof customAnchor === 'string') {
|
|
@@ -73,18 +76,14 @@ export class FloatingRootState {
|
|
|
73
76
|
return customAnchor;
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
return this.triggerSource
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
constructor(anchorName: string) {
|
|
80
|
-
this.anchorName = anchorName;
|
|
79
|
+
return this.triggerSource();
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
get triggerNode() {
|
|
84
|
-
return this.triggerSource;
|
|
82
|
+
get triggerNode(): AnchorNode {
|
|
83
|
+
return this.triggerSource();
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
setCustomAnchorSource(source:
|
|
86
|
+
setCustomAnchorSource(source: () => CustomAnchorNode) {
|
|
88
87
|
this.#customAnchorSource = source;
|
|
89
88
|
}
|
|
90
89
|
}
|
|
@@ -134,12 +133,9 @@ export class FloatingContentState {
|
|
|
134
133
|
#arrowWidth = $state(0);
|
|
135
134
|
#arrowHeight = $state(0);
|
|
136
135
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
arrowRef = simpleBox<HTMLElement | null>(null);
|
|
140
|
-
readonly contentAttachment = attachRef(this.contentRef);
|
|
136
|
+
contentRef = $state.raw<HTMLElement | null>(null);
|
|
137
|
+
readonly contentAttachment = attachRef<HTMLElement>((v) => (this.contentRef = v));
|
|
141
138
|
readonly arrowAttachment = {
|
|
142
|
-
...attachRef(this.arrowRef),
|
|
143
139
|
[createAttachmentKey()]: ((node) => this.#measureArrow(node)) satisfies Attachment<HTMLElement>,
|
|
144
140
|
};
|
|
145
141
|
|
|
@@ -244,7 +240,7 @@ export class FloatingContentState {
|
|
|
244
240
|
/** A virtual anchor (a rect, no element) is rendered as a zero-size fixed box carrying the anchor name. */
|
|
245
241
|
virtualAnchorStyle = $derived.by(() => {
|
|
246
242
|
if (!this.opts.present.current) return null;
|
|
247
|
-
const anchor = this.root.anchorNode
|
|
243
|
+
const anchor = this.root.anchorNode;
|
|
248
244
|
// SSR-safe: on the server there is no HTMLElement (and no virtual anchor either)
|
|
249
245
|
if (!anchor || typeof HTMLElement === 'undefined' || anchor instanceof HTMLElement) return null;
|
|
250
246
|
const rect = anchor.getBoundingClientRect();
|
|
@@ -300,12 +296,12 @@ export class FloatingContentState {
|
|
|
300
296
|
constructor(opts: FloatingContentStateOpts, root: FloatingRootState) {
|
|
301
297
|
this.opts = opts;
|
|
302
298
|
this.root = root;
|
|
303
|
-
this.root.setCustomAnchorSource(opts.customAnchor);
|
|
299
|
+
this.root.setCustomAnchorSource(() => opts.customAnchor.current);
|
|
304
300
|
|
|
305
301
|
// An element anchor that is not the trigger (a `customAnchor`) needs the anchor name too.
|
|
306
302
|
$effect(() => {
|
|
307
|
-
const anchor = this.root.anchorNode
|
|
308
|
-
if (!(anchor instanceof HTMLElement) || anchor === this.root.triggerNode
|
|
303
|
+
const anchor = this.root.anchorNode;
|
|
304
|
+
if (!(anchor instanceof HTMLElement) || anchor === this.root.triggerNode) return;
|
|
309
305
|
const previous = anchor.style.anchorName;
|
|
310
306
|
anchor.style.anchorName = this.root.anchorName;
|
|
311
307
|
return () => {
|
|
@@ -326,7 +322,7 @@ export class FloatingContentState {
|
|
|
326
322
|
this.#measuredPlacement = null;
|
|
327
323
|
return;
|
|
328
324
|
}
|
|
329
|
-
const content = this.contentRef
|
|
325
|
+
const content = this.contentRef;
|
|
330
326
|
if (!content) return;
|
|
331
327
|
const win = getWindow(content);
|
|
332
328
|
let raf = win.requestAnimationFrame(() => this.#measurePlacement());
|
|
@@ -345,8 +341,8 @@ export class FloatingContentState {
|
|
|
345
341
|
}
|
|
346
342
|
|
|
347
343
|
#measurePlacement() {
|
|
348
|
-
const content = this.contentRef
|
|
349
|
-
const anchor = this.root.anchorNode
|
|
344
|
+
const content = this.contentRef;
|
|
345
|
+
const anchor = this.root.anchorNode;
|
|
350
346
|
if (!content || !anchor) return;
|
|
351
347
|
const c = content.getBoundingClientRect();
|
|
352
348
|
const a = anchor.getBoundingClientRect();
|
|
@@ -444,7 +440,7 @@ export class FloatingArrowState {
|
|
|
444
440
|
* tracking the pointer, which the content renders as a zero-size anchor element. Reads the
|
|
445
441
|
* floating-root context, so call it during component init.
|
|
446
442
|
*/
|
|
447
|
-
export function setFloatingAnchor(source:
|
|
443
|
+
export function setFloatingAnchor(source: () => AnchorNode, tooltip = false) {
|
|
448
444
|
const root = tooltip ? getFloatingTooltipRoot() : getFloatingRoot();
|
|
449
445
|
root.triggerSource = source;
|
|
450
446
|
}
|
|
@@ -460,11 +456,11 @@ export function floatingAnchor(tooltip = false): RefAttachment {
|
|
|
460
456
|
return {
|
|
461
457
|
[createAttachmentKey()]: (node) => {
|
|
462
458
|
const el = node as HTMLElement;
|
|
463
|
-
root.triggerSource =
|
|
459
|
+
root.triggerSource = () => el;
|
|
464
460
|
el.style.anchorName = root.anchorName;
|
|
465
461
|
return () => {
|
|
466
462
|
el.style.removeProperty('anchor-name');
|
|
467
|
-
root.triggerSource =
|
|
463
|
+
root.triggerSource = () => null;
|
|
468
464
|
};
|
|
469
465
|
},
|
|
470
466
|
};
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { untrack } from 'svelte';
|
|
2
|
-
import type { ReadableBox, WritableBox } from './tools/index.js';
|
|
3
2
|
|
|
3
|
+
/** The group's bindable value, as an accessor on the group's options. */
|
|
4
4
|
interface GroupValue {
|
|
5
|
-
|
|
5
|
+
value: string[];
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
/** The item's `value` and `checked` as accessors over its props. */
|
|
8
9
|
interface GroupItem {
|
|
9
|
-
value:
|
|
10
|
-
checked:
|
|
10
|
+
readonly value: string | undefined;
|
|
11
|
+
readonly checked: boolean;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -20,17 +21,17 @@ export function joinGroup(group: GroupValue | null, item: GroupItem): () => bool
|
|
|
20
21
|
if (!group) return () => undefined;
|
|
21
22
|
|
|
22
23
|
$effect(() => {
|
|
23
|
-
const checked = item.checked
|
|
24
|
+
const checked = item.checked;
|
|
24
25
|
untrack(() => {
|
|
25
|
-
const value = item.value
|
|
26
|
-
const values = group.
|
|
26
|
+
const value = item.value;
|
|
27
|
+
const values = group.value;
|
|
27
28
|
if (!value || checked === values.includes(value)) return;
|
|
28
|
-
group.
|
|
29
|
+
group.value = checked ? [...values, value] : values.filter((v) => v !== value);
|
|
29
30
|
});
|
|
30
31
|
});
|
|
31
32
|
|
|
32
33
|
return () => {
|
|
33
|
-
const value = item.value
|
|
34
|
-
return value ? group.
|
|
34
|
+
const value = item.value;
|
|
35
|
+
return value ? group.value.includes(value) : undefined;
|
|
35
36
|
};
|
|
36
37
|
}
|
|
@@ -18,13 +18,13 @@ type NativePopoverAnchor = HTMLElement | string | null | undefined | object;
|
|
|
18
18
|
interface NativePopoverContentState {
|
|
19
19
|
readonly root: {
|
|
20
20
|
readonly opts: {
|
|
21
|
-
readonly open:
|
|
22
|
-
readonly onOpenChangeComplete:
|
|
21
|
+
readonly open: boolean;
|
|
22
|
+
readonly onOpenChangeComplete: (open: boolean) => void;
|
|
23
23
|
};
|
|
24
24
|
readonly triggerNode: HTMLElement | null;
|
|
25
25
|
};
|
|
26
26
|
readonly opts: {
|
|
27
|
-
readonly ref:
|
|
27
|
+
readonly ref: HTMLElement | null;
|
|
28
28
|
};
|
|
29
29
|
onEscapeKeydown?: (event: KeyboardEvent) => void;
|
|
30
30
|
onInteractOutside?: (event: PointerEvent) => void;
|
|
@@ -60,8 +60,8 @@ export function useNativePopoverLifecycle(
|
|
|
60
60
|
options: { anchor?: Getter<NativePopoverAnchor>; mode?: 'auto' | 'manual' } = {},
|
|
61
61
|
) {
|
|
62
62
|
const mode = options.mode ?? 'manual';
|
|
63
|
-
const ref = () => state.opts.ref
|
|
64
|
-
const open = () => state.root.opts.open
|
|
63
|
+
const ref = () => state.opts.ref;
|
|
64
|
+
const open = () => state.root.opts.open;
|
|
65
65
|
|
|
66
66
|
// The surface's own animations gate the deferred hide; a long-running descendant
|
|
67
67
|
// animation (a chart entry inside a popover) must not hold the top layer.
|
|
@@ -131,7 +131,7 @@ export function useNativePopoverLifecycle(
|
|
|
131
131
|
});
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
-
useOpenChangeComplete(open, ref, (isOpen) => state.root.opts.onOpenChangeComplete
|
|
134
|
+
useOpenChangeComplete(open, ref, (isOpen) => state.root.opts.onOpenChangeComplete(isOpen));
|
|
135
135
|
|
|
136
136
|
if (mode === 'auto') {
|
|
137
137
|
// The UA closes the popover itself (light dismiss, Escape, another auto popover opening);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { Getter } from './tools/index.js';
|
|
2
2
|
import { getDirectionalKeys, kbd } from './kbd.js';
|
|
3
3
|
import type { Direction, Orientation } from './index.js';
|
|
4
4
|
import { isHTMLElement } from './tools/utils/dom.js';
|
|
@@ -30,21 +30,19 @@ type RovingFocusGroupOptions = (
|
|
|
30
30
|
candidateSelector?: undefined;
|
|
31
31
|
}
|
|
32
32
|
) & {
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
*/
|
|
36
|
-
rootNode: Box<HTMLElement | null>;
|
|
33
|
+
/** The node the candidates live under. */
|
|
34
|
+
rootNode: Getter<HTMLElement | null>;
|
|
37
35
|
|
|
38
36
|
/**
|
|
39
37
|
* Whether to loop through the candidates when reaching the end.
|
|
40
38
|
*/
|
|
41
|
-
loop:
|
|
39
|
+
loop: Getter<boolean>;
|
|
42
40
|
|
|
43
41
|
/**
|
|
44
42
|
* The orientation of the roving focus group. Used
|
|
45
43
|
* to determine how keyboard navigation should work.
|
|
46
44
|
*/
|
|
47
|
-
orientation:
|
|
45
|
+
orientation: Getter<Orientation>;
|
|
48
46
|
|
|
49
47
|
/**
|
|
50
48
|
* A callback function called when a candidate is focused.
|
|
@@ -54,7 +52,7 @@ type RovingFocusGroupOptions = (
|
|
|
54
52
|
|
|
55
53
|
export class RovingFocusGroup {
|
|
56
54
|
readonly #opts: RovingFocusGroupOptions;
|
|
57
|
-
|
|
55
|
+
#currentTabStopId = $state.raw<string | null>(null);
|
|
58
56
|
/** What a candidate of an attribute-marked group stamps on itself; empty for the other kinds. */
|
|
59
57
|
readonly candidateAttrs: Record<string, ''>;
|
|
60
58
|
|
|
@@ -64,7 +62,7 @@ export class RovingFocusGroup {
|
|
|
64
62
|
}
|
|
65
63
|
|
|
66
64
|
getCandidateNodes() {
|
|
67
|
-
const rootNode = this.#opts.rootNode
|
|
65
|
+
const rootNode = this.#opts.rootNode();
|
|
68
66
|
if (!BROWSER || !rootNode) return [];
|
|
69
67
|
if (this.#opts.candidateNodes) return this.#opts.candidateNodes(rootNode);
|
|
70
68
|
|
|
@@ -79,7 +77,7 @@ export class RovingFocusGroup {
|
|
|
79
77
|
}
|
|
80
78
|
|
|
81
79
|
handleKeydown(node: HTMLElement | null | undefined, e: KeyboardEvent, both: boolean = false) {
|
|
82
|
-
const rootNode = this.#opts.rootNode
|
|
80
|
+
const rootNode = this.#opts.rootNode();
|
|
83
81
|
if (!rootNode || !node) return;
|
|
84
82
|
|
|
85
83
|
const items = this.getCandidateNodes();
|
|
@@ -87,8 +85,8 @@ export class RovingFocusGroup {
|
|
|
87
85
|
|
|
88
86
|
const currentIndex = items.indexOf(node);
|
|
89
87
|
const dir = (rootNode.ownerDocument.defaultView ?? window).getComputedStyle(rootNode).direction as Direction;
|
|
90
|
-
const { nextKey, prevKey } = getDirectionalKeys(dir, this.#opts.orientation
|
|
91
|
-
const loop = this.#opts.loop
|
|
88
|
+
const { nextKey, prevKey } = getDirectionalKeys(dir, this.#opts.orientation());
|
|
89
|
+
const loop = this.#opts.loop();
|
|
92
90
|
|
|
93
91
|
const keyToIndex = {
|
|
94
92
|
[nextKey]: currentIndex + 1,
|
|
@@ -117,19 +115,19 @@ export class RovingFocusGroup {
|
|
|
117
115
|
const itemToFocus = items[itemIndex];
|
|
118
116
|
if (!itemToFocus) return;
|
|
119
117
|
itemToFocus.focus();
|
|
120
|
-
this.#currentTabStopId
|
|
118
|
+
this.#currentTabStopId = itemToFocus.id;
|
|
121
119
|
this.#opts.onCandidateFocus?.(itemToFocus);
|
|
122
120
|
return itemToFocus;
|
|
123
121
|
}
|
|
124
122
|
|
|
125
123
|
getTabIndex(node: HTMLElement | null | undefined) {
|
|
126
124
|
const items = this.getCandidateNodes();
|
|
127
|
-
const anyActive = this.#currentTabStopId
|
|
125
|
+
const anyActive = this.#currentTabStopId !== null;
|
|
128
126
|
|
|
129
127
|
if (node && !anyActive && items[0] === node) {
|
|
130
|
-
this.#currentTabStopId
|
|
128
|
+
this.#currentTabStopId = node.id;
|
|
131
129
|
return 0;
|
|
132
|
-
} else if (node?.id === this.#currentTabStopId
|
|
130
|
+
} else if (node?.id === this.#currentTabStopId) {
|
|
133
131
|
return 0;
|
|
134
132
|
}
|
|
135
133
|
|
|
@@ -137,13 +135,13 @@ export class RovingFocusGroup {
|
|
|
137
135
|
}
|
|
138
136
|
|
|
139
137
|
setCurrentTabStopId(id: string) {
|
|
140
|
-
this.#currentTabStopId
|
|
138
|
+
this.#currentTabStopId = id;
|
|
141
139
|
}
|
|
142
140
|
|
|
143
141
|
focusCurrentTabStop() {
|
|
144
|
-
const currentTabStopId = this.#currentTabStopId
|
|
142
|
+
const currentTabStopId = this.#currentTabStopId;
|
|
145
143
|
if (!currentTabStopId) return;
|
|
146
|
-
const currentTabStop = this.#opts.rootNode
|
|
144
|
+
const currentTabStop = this.#opts.rootNode()?.querySelector(`#${currentTabStopId}`);
|
|
147
145
|
if (!currentTabStop || !isHTMLElement(currentTabStop)) return;
|
|
148
146
|
currentTabStop.focus();
|
|
149
147
|
}
|
|
@@ -1,21 +1,16 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import type { RovingFocusGroup } from './roving-focus-group.js';
|
|
3
|
-
|
|
4
|
-
type MaybeReadableBoolean = boolean | ReadableBox<boolean>;
|
|
1
|
+
import { type Getter, type RefAttachment, attachRef } from './tools/index.js';
|
|
2
|
+
import type { RovingFocusGroup } from './roving-focus-group.svelte.js';
|
|
5
3
|
|
|
6
4
|
type RovingFocusItemOptions<T extends HTMLElement = HTMLElement> = {
|
|
7
5
|
group: RovingFocusGroup;
|
|
8
|
-
ref:
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
ref: Getter<T | null>;
|
|
7
|
+
/** Receives the node from the attachment; the owner keeps it wherever its `ref` reads from. */
|
|
8
|
+
setRef: (node: T | null) => void;
|
|
9
|
+
/** Whether arrow keys move from this item; absent means always. */
|
|
10
|
+
enabled?: Getter<boolean>;
|
|
11
11
|
onRefChange?: (node: T | null) => void;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
function readBoolean(value: MaybeReadableBoolean | undefined, fallback: boolean) {
|
|
15
|
-
if (value === undefined) return fallback;
|
|
16
|
-
return typeof value === 'boolean' ? value : value.current;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
14
|
export class RovingFocusItem<T extends HTMLElement = HTMLElement> {
|
|
20
15
|
readonly #opts: RovingFocusItemOptions<T>;
|
|
21
16
|
readonly attachment: RefAttachment<T>;
|
|
@@ -23,22 +18,22 @@ export class RovingFocusItem<T extends HTMLElement = HTMLElement> {
|
|
|
23
18
|
|
|
24
19
|
constructor(opts: RovingFocusItemOptions<T>) {
|
|
25
20
|
this.#opts = opts;
|
|
26
|
-
this.attachment = attachRef(opts.
|
|
21
|
+
this.attachment = attachRef<T>(opts.setRef, opts.onRefChange);
|
|
27
22
|
this.handleKeydown = this.handleKeydown.bind(this);
|
|
28
23
|
|
|
29
24
|
$effect(() => {
|
|
30
|
-
if (!
|
|
25
|
+
if (!(this.#opts.enabled?.() ?? true)) {
|
|
31
26
|
this.#tabIndex = 0;
|
|
32
27
|
return;
|
|
33
28
|
}
|
|
34
29
|
|
|
35
|
-
this.#tabIndex = this.#opts.group.getTabIndex(this.#opts.ref
|
|
30
|
+
this.#tabIndex = this.#opts.group.getTabIndex(this.#opts.ref());
|
|
36
31
|
});
|
|
37
32
|
}
|
|
38
33
|
|
|
39
34
|
handleKeydown(e: KeyboardEvent) {
|
|
40
|
-
if (!
|
|
41
|
-
return this.#opts.group.handleKeydown(this.#opts.ref
|
|
35
|
+
if (!(this.#opts.enabled?.() ?? true)) return;
|
|
36
|
+
return this.#opts.group.handleKeydown(this.#opts.ref(), e);
|
|
42
37
|
}
|
|
43
38
|
|
|
44
39
|
readonly props = $derived.by(
|
|
@@ -1,83 +1,95 @@
|
|
|
1
|
-
import type { ReadableBox, ReadableBoxedValues, WritableBox } from './tools/index.js';
|
|
2
1
|
import { boolToStr, boolToEmptyStrOrUndef, boolToTrueOrUndef, getAriaChecked } from './attrs.js';
|
|
3
2
|
import { kbd } from './kbd.js';
|
|
4
3
|
import type { Orientation } from './index.js';
|
|
5
|
-
import type { BitsKeyboardEvent, BitsMouseEvent
|
|
6
|
-
import type { RovingFocusGroup } from './roving-focus-group.js';
|
|
4
|
+
import type { BitsKeyboardEvent, BitsMouseEvent } from './types.js';
|
|
5
|
+
import type { RovingFocusGroup } from './roving-focus-group.svelte.js';
|
|
7
6
|
import { RovingFocusItem } from './roving-focus-item.svelte.js';
|
|
8
7
|
|
|
9
8
|
export type SelectionType = 'single' | 'multiple';
|
|
10
9
|
|
|
11
|
-
export
|
|
10
|
+
export type Selection = string | string[];
|
|
11
|
+
|
|
12
|
+
export const emptySelection = (type: SelectionType): Selection => (type === 'single' ? '' : []);
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* Membership over a bindable that is a string in single mode and a string array
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Membership over a consumer's bindable that is a string in single mode and a string array
|
|
16
|
+
* in multiple mode. `value` reads and writes the root's prop through the closures it was
|
|
17
|
+
* built with; the mode is fixed at construction, so `with` always keeps the declared shape.
|
|
17
18
|
*/
|
|
18
19
|
export class SelectionValue {
|
|
19
20
|
readonly isMulti: boolean;
|
|
20
|
-
readonly #
|
|
21
|
+
readonly #read: () => Selection;
|
|
22
|
+
readonly #write: (value: Selection) => void;
|
|
21
23
|
|
|
22
|
-
constructor(type: SelectionType, value:
|
|
24
|
+
constructor(type: SelectionType, read: () => Selection, write: (value: Selection) => void) {
|
|
23
25
|
this.isMulti = type === 'multiple';
|
|
24
|
-
this.#
|
|
26
|
+
this.#read = read;
|
|
27
|
+
this.#write = write;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get value(): Selection {
|
|
31
|
+
return this.#read();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
set value(next: Selection) {
|
|
35
|
+
this.#write(next);
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
includes(item: string): boolean {
|
|
28
|
-
const value = this.#
|
|
39
|
+
const value = this.#read();
|
|
29
40
|
return this.isMulti ? (value as string[]).includes(item) : value === item;
|
|
30
41
|
}
|
|
31
42
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
this.#value.current = selected ? item : '';
|
|
40
|
-
}
|
|
41
|
-
return selected;
|
|
43
|
+
/** The value with `item` present or absent; the same reference when it already is. */
|
|
44
|
+
with(item: string, selected: boolean): Selection {
|
|
45
|
+
const value = this.#read();
|
|
46
|
+
const present = this.isMulti ? (value as string[]).includes(item) : value === item;
|
|
47
|
+
if (present === selected) return value;
|
|
48
|
+
if (!this.isMulti) return selected ? item : '';
|
|
49
|
+
return selected ? [...(value as string[]), item] : (value as string[]).filter((v) => v !== item);
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* What a selection item needs from the group that owns it. Reactive members are accessors on the
|
|
55
|
+
* implementing class, read at use, never copied out.
|
|
56
|
+
*/
|
|
46
57
|
export interface SelectionGroup {
|
|
47
58
|
readonly selection: SelectionValue;
|
|
48
|
-
readonly disabled:
|
|
49
|
-
readonly orientation:
|
|
59
|
+
readonly disabled: boolean;
|
|
60
|
+
readonly orientation: Orientation;
|
|
50
61
|
readonly rovingFocusGroup: RovingFocusGroup;
|
|
51
62
|
/** Whether arrow keys move between the items; absent means always. */
|
|
52
|
-
readonly rovingFocus?:
|
|
63
|
+
readonly rovingFocus?: boolean;
|
|
53
64
|
/** A selection takes the roving tab stop in a toggle group; a toolbar's stays where it is. */
|
|
54
65
|
readonly selectionTakesTabStop: boolean;
|
|
55
66
|
/** Beyond the roving group's candidate attribute, which every item carries. */
|
|
56
67
|
readonly extraItemAttrs?: Record<string, ''>;
|
|
57
68
|
}
|
|
58
69
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
/** The item component's props as accessors over its `$props()`; `ref` writes back to its bindable. */
|
|
71
|
+
export interface SelectionItemOpts {
|
|
72
|
+
readonly id: string;
|
|
73
|
+
readonly value: string;
|
|
74
|
+
readonly disabled: boolean;
|
|
75
|
+
ref: HTMLElement | null;
|
|
76
|
+
}
|
|
66
77
|
|
|
67
78
|
export class SelectionItemState {
|
|
68
79
|
readonly opts: SelectionItemOpts;
|
|
69
80
|
readonly group: SelectionGroup;
|
|
70
81
|
readonly rovingItem: RovingFocusItem;
|
|
71
|
-
readonly #isDisabled = $derived.by(() => this.opts.disabled
|
|
72
|
-
readonly isPressed = $derived.by(() => this.group.selection.includes(this.opts.value
|
|
82
|
+
readonly #isDisabled = $derived.by(() => this.opts.disabled || this.group.disabled);
|
|
83
|
+
readonly isPressed = $derived.by(() => this.group.selection.includes(this.opts.value));
|
|
73
84
|
|
|
74
85
|
constructor(opts: SelectionItemOpts, group: SelectionGroup) {
|
|
75
86
|
this.opts = opts;
|
|
76
87
|
this.group = group;
|
|
77
88
|
this.rovingItem = new RovingFocusItem({
|
|
78
89
|
group: group.rovingFocusGroup,
|
|
79
|
-
ref: opts.ref,
|
|
80
|
-
|
|
90
|
+
ref: () => opts.ref,
|
|
91
|
+
setRef: (v) => (opts.ref = v),
|
|
92
|
+
enabled: () => group.rovingFocus ?? true,
|
|
81
93
|
});
|
|
82
94
|
|
|
83
95
|
this.onclick = this.onclick.bind(this);
|
|
@@ -86,8 +98,10 @@ export class SelectionItemState {
|
|
|
86
98
|
|
|
87
99
|
#activate() {
|
|
88
100
|
if (this.#isDisabled) return;
|
|
89
|
-
const
|
|
90
|
-
|
|
101
|
+
const item = this.opts.value;
|
|
102
|
+
const selected = !this.group.selection.includes(item);
|
|
103
|
+
this.group.selection.value = this.group.selection.with(item, selected);
|
|
104
|
+
if (selected && this.group.selectionTakesTabStop) this.group.rovingFocusGroup.setCurrentTabStopId(this.opts.id);
|
|
91
105
|
}
|
|
92
106
|
|
|
93
107
|
onclick(_: BitsMouseEvent) {
|
|
@@ -109,12 +123,12 @@ export class SelectionItemState {
|
|
|
109
123
|
readonly props = $derived.by(
|
|
110
124
|
() =>
|
|
111
125
|
({
|
|
112
|
-
id: this.opts.id
|
|
126
|
+
id: this.opts.id,
|
|
113
127
|
role: this.group.selection.isMulti ? undefined : 'radio',
|
|
114
|
-
'data-orientation': this.group.orientation
|
|
128
|
+
'data-orientation': this.group.orientation,
|
|
115
129
|
'data-disabled': boolToEmptyStrOrUndef(this.#isDisabled),
|
|
116
130
|
'data-state': this.isPressed ? 'on' : 'off',
|
|
117
|
-
'data-value': this.opts.value
|
|
131
|
+
'data-value': this.opts.value,
|
|
118
132
|
'aria-pressed': this.group.selection.isMulti ? boolToStr(this.isPressed) : undefined,
|
|
119
133
|
'aria-checked': this.group.selection.isMulti ? undefined : getAriaChecked(this.isPressed, false),
|
|
120
134
|
disabled: boolToTrueOrUndef(this.#isDisabled),
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { simpleBox, type WritableBox } from './tools/index.js';
|
|
2
|
-
|
|
3
1
|
interface Machine<S> {
|
|
4
2
|
[k: string]: { [k: string]: S };
|
|
5
3
|
}
|
|
@@ -11,22 +9,22 @@ type MachineEvent<T> = keyof UnionToIntersection<T[keyof T]>;
|
|
|
11
9
|
type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (x: infer R) => any ? R : never;
|
|
12
10
|
|
|
13
11
|
export class StateMachine<M> {
|
|
14
|
-
|
|
12
|
+
state: MachineState<M>;
|
|
15
13
|
readonly #machine: M & Machine<MachineState<M>>;
|
|
16
14
|
|
|
17
15
|
constructor(initialState: MachineState<M>, machine: M & Machine<MachineState<M>>) {
|
|
18
|
-
this.state =
|
|
16
|
+
this.state = $state.raw(initialState);
|
|
19
17
|
this.#machine = machine;
|
|
20
18
|
this.dispatch = this.dispatch.bind(this);
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
#reducer(event: MachineEvent<M>) {
|
|
24
|
-
// @ts-expect-error state
|
|
25
|
-
const nextState = this.#machine[this.state
|
|
26
|
-
return nextState ?? this.state
|
|
22
|
+
// @ts-expect-error state is keyof M
|
|
23
|
+
const nextState = this.#machine[this.state][event];
|
|
24
|
+
return nextState ?? this.state;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
dispatch(event: MachineEvent<M>) {
|
|
30
|
-
this.state
|
|
28
|
+
this.state = this.#reducer(event);
|
|
31
29
|
}
|
|
32
30
|
}
|
|
@@ -4,20 +4,22 @@ import type { AnyFn } from './types.js';
|
|
|
4
4
|
* A self-cancelling timeout. `start()` (re)schedules `cb` after `getInterval()` ms,
|
|
5
5
|
* `stop()` clears it; auto-stops when the owning component/effect is destroyed.
|
|
6
6
|
*/
|
|
7
|
-
export function createEffectTimeout<T extends AnyFn>(cb: T, getDelay: () => number) {
|
|
7
|
+
export function createEffectTimeout<T extends AnyFn>(cb: T, getDelay: () => number, getWindow: () => typeof globalThis = () => globalThis) {
|
|
8
8
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
9
|
+
let timerWindow: typeof globalThis;
|
|
9
10
|
|
|
10
11
|
const stop = () => {
|
|
11
12
|
if (timer === undefined) return;
|
|
12
13
|
|
|
13
|
-
clearTimeout(timer);
|
|
14
|
+
timerWindow.clearTimeout(timer);
|
|
14
15
|
timer = undefined;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
const start = (...args: Parameters<T>) => {
|
|
18
19
|
stop();
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
timerWindow = getWindow();
|
|
22
|
+
timer = timerWindow.setTimeout(() => {
|
|
21
23
|
timer = undefined;
|
|
22
24
|
cb(...args);
|
|
23
25
|
}, getDelay());
|
|
@@ -33,4 +35,4 @@ export function createEffectTimeout<T extends AnyFn>(cb: T, getDelay: () => numb
|
|
|
33
35
|
return timer !== undefined;
|
|
34
36
|
},
|
|
35
37
|
};
|
|
36
|
-
}
|
|
38
|
+
}
|