sheer-ui 0.3.1 → 0.4.2

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.
Files changed (58) hide show
  1. package/package.json +1 -1
  2. package/src/lib/components/accordion/components/accordion-item.svelte +3 -3
  3. package/src/lib/components/accordion/components/accordion.svelte +29 -13
  4. package/src/lib/components/accordion/types.ts +3 -3
  5. package/src/lib/components/checkbox/components/checkbox-group.svelte +7 -5
  6. package/src/lib/components/checkbox/components/checkbox.svelte +12 -8
  7. package/src/lib/components/combobox/components/combobox.svelte +2 -2
  8. package/src/lib/components/combobox/select/components/select.svelte +2 -2
  9. package/src/lib/components/context-menu/components/context-menu-trigger.svelte +1 -1
  10. package/src/lib/components/data-table/table-faceted-filter.svelte +2 -2
  11. package/src/lib/components/data-table/table-toolbar.svelte +5 -5
  12. package/src/lib/components/date-field/date-field.svelte.ts +2 -3
  13. package/src/lib/components/menu/components/menu-checkbox-group.svelte +6 -4
  14. package/src/lib/components/menu/components/menu-checkbox-item.svelte +11 -7
  15. package/src/lib/components/menu/menu.svelte.ts +9 -18
  16. package/src/lib/components/menubar/menubar.svelte.ts +7 -6
  17. package/src/lib/components/navigation-menu/components/navigation-menu-content.svelte +1 -1
  18. package/src/lib/components/navigation-menu/components/navigation-menu-indicator.svelte +2 -2
  19. package/src/lib/components/navigation-menu/components/navigation-menu-trigger.svelte +1 -1
  20. package/src/lib/components/navigation-menu/navigation-menu.svelte.ts +57 -60
  21. package/src/lib/components/pagination/components/pagination-next-button.svelte +12 -7
  22. package/src/lib/components/pagination/components/pagination-page.svelte +15 -8
  23. package/src/lib/components/pagination/components/pagination-prev-button.svelte +12 -7
  24. package/src/lib/components/pagination/components/pagination.svelte +31 -18
  25. package/src/lib/components/pagination/pagination.svelte.ts +48 -64
  26. package/src/lib/components/pin-input/pin-input.svelte.ts +7 -8
  27. package/src/lib/components/pin-input/usePasswordManager.svelte.ts +2 -2
  28. package/src/lib/components/scroll-area/scroll-area.svelte.ts +9 -10
  29. package/src/lib/components/slider/components/slider.svelte +2 -1
  30. package/src/lib/components/tabs/components/tabs-content.svelte +12 -7
  31. package/src/lib/components/tabs/components/tabs-list.svelte +9 -6
  32. package/src/lib/components/tabs/components/tabs-trigger.svelte +15 -8
  33. package/src/lib/components/tabs/components/tabs.svelte +28 -17
  34. package/src/lib/components/tabs/tabs.svelte.ts +51 -67
  35. package/src/lib/components/time-field/time-field.svelte.ts +2 -2
  36. package/src/lib/components/toggle-group/components/toggle-group-item.svelte +15 -8
  37. package/src/lib/components/toggle-group/components/toggle-group.svelte +28 -16
  38. package/src/lib/components/toggle-group/toggle-group.svelte.ts +33 -35
  39. package/src/lib/components/toolbar/components/toolbar-group-item.svelte +15 -8
  40. package/src/lib/components/toolbar/components/toolbar-group.svelte +19 -14
  41. package/src/lib/components/toolbar/toolbar.svelte.ts +28 -30
  42. package/src/lib/components/tooltip/tooltip.svelte.ts +6 -27
  43. package/src/lib/internal/dismissible-layer/use-dismissable-layer.svelte.ts +3 -4
  44. package/src/lib/internal/floating-layer/use-floating-layer.svelte.ts +26 -30
  45. package/src/lib/internal/{roving-focus-group.ts → roving-focus-group.svelte.ts} +17 -19
  46. package/src/lib/internal/roving-focus-item.svelte.ts +12 -17
  47. package/src/lib/internal/selection.svelte.ts +55 -41
  48. package/src/lib/internal/{state-machine.ts → state-machine.svelte.ts} +6 -8
  49. package/src/lib/internal/table/table.svelte.ts +23 -43
  50. package/src/lib/internal/timeout-fn.svelte.ts +6 -4
  51. package/src/lib/internal/tools/box.svelte.ts +5 -114
  52. package/src/lib/internal/tools/index.ts +1 -11
  53. package/src/lib/internal/tools/types.ts +0 -3
  54. package/src/lib/internal/typeahead.svelte.ts +9 -13
  55. package/src/lib/internal/types.ts +6 -0
  56. package/src/lib/internal/vaul/use-drawer-content.svelte.ts +2 -2
  57. package/src/lib/internal/vaul/use-drawer-root.svelte.ts +12 -7
  58. package/src/lib/components/accordion/accordion.svelte.ts +0 -43
@@ -1,17 +1,11 @@
1
1
  import { createContext } from 'svelte';
2
- import { type ReadableBox, type ReadableBoxedValues, type WritableBox, attachRef } from '../../internal/tools/index.js';
2
+ import { type ReadableBox, type ReadableBoxedValues, attachRef } from '../../internal/tools/index.js';
3
3
  import { createBitsAttrs, boolToEmptyStrOrUndef, boolToTrueOrUndef } from '../../internal/attrs.js';
4
4
  import type { Orientation } from '../../internal/index.js';
5
5
  import type { BitsKeyboardEvent, RefAttachment, WithRefOpts } from '../../internal/types.js';
6
- import { RovingFocusGroup } from '../../internal/roving-focus-group.js';
6
+ import { RovingFocusGroup } from '../../internal/roving-focus-group.svelte.js';
7
7
  import { RovingFocusItem } from '../../internal/roving-focus-item.svelte.js';
8
- import {
9
- type SelectionGroup,
10
- type SelectionItemOpts,
11
- type SelectionType,
12
- SelectionItemState,
13
- SelectionValue,
14
- } from '../../internal/selection.svelte.js';
8
+ import { type SelectionGroup, type SelectionItemOpts, SelectionItemState, SelectionValue } from '../../internal/selection.svelte.js';
15
9
 
16
10
  export const toolbarAttrs = createBitsAttrs({
17
11
  component: 'toolbar',
@@ -41,9 +35,9 @@ export class ToolbarRootState {
41
35
  this.attachment = attachRef(this.opts.ref);
42
36
 
43
37
  this.rovingFocusGroup = new RovingFocusGroup({
44
- orientation: this.opts.orientation,
45
- loop: this.opts.loop,
46
- rootNode: this.opts.ref,
38
+ orientation: () => this.opts.orientation.current,
39
+ loop: () => this.opts.loop.current,
40
+ rootNode: () => this.opts.ref.current,
47
41
  candidateAttr: toolbarAttrs.item,
48
42
  });
49
43
  }
@@ -60,14 +54,12 @@ export class ToolbarRootState {
60
54
  );
61
55
  }
62
56
 
63
- interface ToolbarGroupStateOpts
64
- extends
65
- WithRefOpts,
66
- ReadableBoxedValues<{
67
- disabled: boolean;
68
- }> {
69
- type: SelectionType;
70
- value: WritableBox<string | string[]>;
57
+ /** The group component's props as accessors over its `$props()`; `ref` writes back to its bindable. */
58
+ interface ToolbarGroupStateOpts {
59
+ readonly id: string;
60
+ readonly disabled: boolean;
61
+ ref: HTMLElement | null;
62
+ selection: SelectionValue;
71
63
  }
72
64
 
73
65
  /** A selection group inside the toolbar: its items rove with the toolbar's other items. */
@@ -78,8 +70,6 @@ export class ToolbarGroupState implements SelectionGroup {
78
70
  readonly opts: ToolbarGroupStateOpts;
79
71
  readonly root: ToolbarRootState;
80
72
  readonly selection: SelectionValue;
81
- readonly disabled: ReadableBox<boolean>;
82
- readonly orientation: ReadableBox<Orientation>;
83
73
  readonly rovingFocusGroup: RovingFocusGroup;
84
74
  readonly selectionTakesTabStop = false;
85
75
  readonly extraItemAttrs = { [toolbarAttrs['group-item']]: '' } as const;
@@ -88,21 +78,27 @@ export class ToolbarGroupState implements SelectionGroup {
88
78
  constructor(opts: ToolbarGroupStateOpts, root: ToolbarRootState) {
89
79
  this.opts = opts;
90
80
  this.root = root;
91
- this.selection = new SelectionValue(opts.type, opts.value);
92
- this.disabled = opts.disabled;
93
- this.orientation = root.opts.orientation;
81
+ this.selection = opts.selection;
94
82
  this.rovingFocusGroup = root.rovingFocusGroup;
95
- this.attachment = attachRef(this.opts.ref);
83
+ this.attachment = attachRef<HTMLElement>((v) => (opts.ref = v));
84
+ }
85
+
86
+ get disabled() {
87
+ return this.opts.disabled;
88
+ }
89
+
90
+ get orientation() {
91
+ return this.root.opts.orientation.current;
96
92
  }
97
93
 
98
94
  readonly props = $derived.by(
99
95
  () =>
100
96
  ({
101
- id: this.opts.id.current,
97
+ id: this.opts.id,
102
98
  [toolbarAttrs.group]: '',
103
99
  role: 'group',
104
100
  'data-orientation': this.root.opts.orientation.current,
105
- 'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current),
101
+ 'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled),
106
102
  ...this.attachment,
107
103
  }) as const,
108
104
  );
@@ -127,7 +123,8 @@ export class ToolbarLinkState {
127
123
  this.root = root;
128
124
  this.rovingItem = new RovingFocusItem({
129
125
  group: this.root.rovingFocusGroup,
130
- ref: this.opts.ref,
126
+ ref: () => this.opts.ref.current,
127
+ setRef: (v) => (this.opts.ref.current = v),
131
128
  });
132
129
 
133
130
  this.onkeydown = this.onkeydown.bind(this);
@@ -179,7 +176,8 @@ export class ToolbarButtonState {
179
176
  this.root = root;
180
177
  this.rovingItem = new RovingFocusItem({
181
178
  group: this.root.rovingFocusGroup,
182
- ref: this.opts.ref,
179
+ ref: () => this.opts.ref.current,
180
+ setRef: (v) => (this.opts.ref.current = v),
183
181
  });
184
182
 
185
183
  this.onkeydown = this.onkeydown.bind(this);
@@ -1,4 +1,4 @@
1
- import { attachRef, DOMContext, type WritableBoxedValues, type ReadableBoxedValues, simpleBox } from '../../internal/tools/index.js';
1
+ import { attachRef, DOMContext, type WritableBoxedValues, type ReadableBoxedValues } from '../../internal/tools/index.js';
2
2
  import { on } from 'svelte/events';
3
3
  import { createContext, onMount, untrack } from 'svelte';
4
4
  import { isElement } from '../../internal/tools/utils/dom.js';
@@ -145,7 +145,6 @@ export class TooltipProviderState {
145
145
  }
146
146
  readonly opts: TooltipProviderStateOpts;
147
147
  isOpenDelayed = $state<boolean>(true);
148
- isPointerInTransit = simpleBox(false);
149
148
  readonly #timerFn: ReturnType<typeof createEffectTimeout<() => void>>;
150
149
  #openTooltip = $state<TooltipRootState | null>(null);
151
150
 
@@ -436,18 +435,9 @@ export class TooltipTriggerState {
436
435
  readonly root: TooltipRootState | null;
437
436
  readonly tether: TooltipTetherState | null;
438
437
  readonly attachment: RefAttachment;
439
- #isPointerDown = simpleBox(false);
438
+ #isPointerDown = false;
440
439
  #hasPointerMoveOpened = $state(false);
441
440
  domContext: DOMContext;
442
- // Fired on pointerenter while the pointer is in transit: if still in transit after the delay, the
443
- // user is likely staying on this trigger.
444
- #transitCheck = createEffectTimeout(() => {
445
- const root = this.#getRoot();
446
- if (!root || !root.provider.isPointerInTransit.current) return;
447
- root.provider.isPointerInTransit.current = false;
448
- root.onTriggerEnter(this.opts.id.current);
449
- this.#hasPointerMoveOpened = true;
450
- }, () => 250);
451
441
  #mounted = false;
452
442
  #lastRegisteredId: string | null = null;
453
443
 
@@ -551,17 +541,17 @@ export class TooltipTriggerState {
551
541
  };
552
542
 
553
543
  handlePointerUp = () => {
554
- this.#isPointerDown.current = false;
544
+ this.#isPointerDown = false;
555
545
  };
556
546
 
557
547
  #onpointerup: PointerEventHandler<HTMLElement> = () => {
558
548
  if (this.#isDisabled()) return;
559
- this.#isPointerDown.current = false;
549
+ this.#isPointerDown = false;
560
550
  };
561
551
 
562
552
  #onpointerdown: PointerEventHandler<HTMLElement> = () => {
563
553
  if (this.#isDisabled()) return;
564
- this.#isPointerDown.current = true;
554
+ this.#isPointerDown = true;
565
555
 
566
556
  on(
567
557
  this.domContext.getDocument(),
@@ -584,12 +574,6 @@ export class TooltipTriggerState {
584
574
  }
585
575
  if (e.pointerType === 'touch') return;
586
576
 
587
- // if in transit, wait briefly to see if user is actually heading to old content or staying here
588
- if (root.provider.isPointerInTransit.current) {
589
- this.#transitCheck.start();
590
- return;
591
- }
592
-
593
577
  root.onTriggerEnter(this.opts.id.current);
594
578
  this.#hasPointerMoveOpened = true;
595
579
  };
@@ -606,10 +590,6 @@ export class TooltipTriggerState {
606
590
  if (e.pointerType === 'touch') return;
607
591
  if (this.#hasPointerMoveOpened) return;
608
592
 
609
- // moving within trigger means we're definitely not in transit anymore
610
- this.#transitCheck.stop();
611
- root.provider.isPointerInTransit.current = false;
612
-
613
593
  root.onTriggerEnter(this.opts.id.current);
614
594
  this.#hasPointerMoveOpened = true;
615
595
  };
@@ -618,7 +598,6 @@ export class TooltipTriggerState {
618
598
  const root = this.#getRoot();
619
599
  if (!root) return;
620
600
  if (this.#isDisabled()) return;
621
- this.#transitCheck.stop();
622
601
  if (!root.isActiveTrigger(this.opts.id.current)) {
623
602
  this.#hasPointerMoveOpened = false;
624
603
  return;
@@ -649,7 +628,7 @@ export class TooltipTriggerState {
649
628
  #onfocus: FocusEventHandler<HTMLElement> = (e) => {
650
629
  const root = this.#getRoot();
651
630
  if (!root) return;
652
- if (this.#isPointerDown.current) return;
631
+ if (this.#isPointerDown) return;
653
632
  if (this.#isDisabled()) {
654
633
  if (root.opts.open.current) {
655
634
  root.handleClose();
@@ -1,5 +1,4 @@
1
1
  import {
2
- simpleBox,
3
2
  boxWith,
4
3
  attachRef,
5
4
  type Getter,
@@ -219,7 +218,7 @@ export function interactOutsideAttachment(opts: {
219
218
  enabled: Getter<boolean>;
220
219
  isValidEvent?: Getter<((e: PointerEvent, node: HTMLElement) => boolean) | undefined>;
221
220
  }): { props: { onfocuscapture: () => void; onblurcapture: () => void }; attachment: RefAttachment<HTMLElement> } {
222
- const ref = simpleBox<HTMLElement | null>(null);
221
+ let node = $state.raw<HTMLElement | null>(null);
223
222
  const state = DismissibleLayerState.create({
224
223
  id: boxWith(opts.id),
225
224
  interactOutsideBehavior: boxWith(opts.interactOutsideBehavior),
@@ -227,9 +226,9 @@ export function interactOutsideAttachment(opts: {
227
226
  enabled: boxWith(opts.enabled),
228
227
  onFocusOutside: boxWith(() => opts.onFocusOutside?.()),
229
228
  isValidEvent: boxWith(() => opts.isValidEvent?.()),
230
- ref,
229
+ ref: boxWith(() => node, (v) => (node = v)),
231
230
  });
232
- return { props: state.props, attachment: attachRef(ref) };
231
+ return { props: state.props, attachment: attachRef((v) => (node = v)) };
233
232
  }
234
233
 
235
234
  function isValidEvent(e: PointerEvent, node: HTMLElement): boolean {
@@ -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
- #customAnchorSource = $state<ReadableBox<CustomAnchorNode>>(simpleBox(null));
64
- triggerSource = $state<ReadableBox<AnchorNode>>(simpleBox(null));
65
- anchorNode: ReadableBox<AnchorNode> = boxWith(() => {
66
- const customAnchor = this.#customAnchorSource.current;
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.current;
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: ReadableBox<CustomAnchorNode>) {
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
- // nodes
138
- contentRef = simpleBox<HTMLElement | null>(null);
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.current;
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.current;
308
- if (!(anchor instanceof HTMLElement) || anchor === this.root.triggerNode.current) return;
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.current;
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.current;
349
- const anchor = this.root.anchorNode.current;
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: ReadableBox<AnchorNode>, tooltip = false) {
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 = simpleBox(el);
459
+ root.triggerSource = () => el;
464
460
  el.style.anchorName = root.anchorName;
465
461
  return () => {
466
462
  el.style.removeProperty('anchor-name');
467
- root.triggerSource = simpleBox(null);
463
+ root.triggerSource = () => null;
468
464
  };
469
465
  },
470
466
  };
@@ -1,4 +1,4 @@
1
- import { type Box, type ReadableBox, simpleBox } from './tools/index.js';
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
- * The id of the root node
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: ReadableBox<boolean>;
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: ReadableBox<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
- readonly #currentTabStopId = simpleBox<string | null>(null);
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.current;
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.current;
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.current);
91
- const loop = this.#opts.loop.current;
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.current = itemToFocus.id;
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.current !== null;
125
+ const anyActive = this.#currentTabStopId !== null;
128
126
 
129
127
  if (node && !anyActive && items[0] === node) {
130
- this.#currentTabStopId.current = node.id;
128
+ this.#currentTabStopId = node.id;
131
129
  return 0;
132
- } else if (node?.id === this.#currentTabStopId.current) {
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.current = id;
138
+ this.#currentTabStopId = id;
141
139
  }
142
140
 
143
141
  focusCurrentTabStop() {
144
- const currentTabStopId = this.#currentTabStopId.current;
142
+ const currentTabStopId = this.#currentTabStopId;
145
143
  if (!currentTabStopId) return;
146
- const currentTabStop = this.#opts.rootNode.current?.querySelector(`#${currentTabStopId}`);
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 ReadableBox, type RefAttachment, type WritableBox, attachRef } from './tools/index.js';
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: WritableBox<T | null>;
9
- enabled?: MaybeReadableBoolean;
10
- both?: MaybeReadableBoolean;
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.ref, opts.onRefChange);
21
+ this.attachment = attachRef<T>(opts.setRef, opts.onRefChange);
27
22
  this.handleKeydown = this.handleKeydown.bind(this);
28
23
 
29
24
  $effect(() => {
30
- if (!readBoolean(this.#opts.enabled, true)) {
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.current);
30
+ this.#tabIndex = this.#opts.group.getTabIndex(this.#opts.ref());
36
31
  });
37
32
  }
38
33
 
39
34
  handleKeydown(e: KeyboardEvent) {
40
- if (!readBoolean(this.#opts.enabled, true)) return;
41
- return this.#opts.group.handleKeydown(this.#opts.ref.current, e, readBoolean(this.#opts.both, false));
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(