bits-ui 1.0.0-next.75 → 1.0.0-next.76

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.
@@ -20,7 +20,7 @@ class AccordionBaseState {
20
20
  this.disabled = props.disabled;
21
21
  this.#ref = props.ref;
22
22
  useRefById({
23
- id: props.id,
23
+ id: this.#id,
24
24
  ref: this.#ref,
25
25
  });
26
26
  this.orientation = props.orientation;
@@ -63,12 +63,10 @@ declare class DialogCloseState {
63
63
  #private;
64
64
  constructor(props: DialogCloseStateProps, root: DialogRootState);
65
65
  onclick(e: BitsMouseEvent): void;
66
- onpointerdown(e: BitsPointerEvent): void;
67
66
  onkeydown(e: BitsKeyboardEvent): void;
68
67
  props: {
69
68
  readonly "data-state": "open" | "closed";
70
69
  readonly id: string;
71
- readonly onpointerdown: (e: BitsPointerEvent) => void;
72
70
  readonly onclick: (e: BitsMouseEvent) => void;
73
71
  readonly onkeydown: (e: BitsKeyboardEvent) => void;
74
72
  };
@@ -146,12 +144,10 @@ declare class AlertDialogCancelState {
146
144
  #private;
147
145
  constructor(props: AlertDialogCancelStateProps, root: DialogRootState);
148
146
  onclick(e: BitsMouseEvent): void;
149
- onpointerdown(e: BitsPointerEvent): void;
150
147
  onkeydown(e: BitsKeyboardEvent): void;
151
148
  props: {
152
149
  readonly "data-state": "open" | "closed";
153
150
  readonly id: string;
154
- readonly onpointerdown: (e: BitsPointerEvent) => void;
155
151
  readonly onclick: (e: BitsMouseEvent) => void;
156
152
  readonly onkeydown: (e: BitsKeyboardEvent) => void;
157
153
  };
@@ -119,7 +119,6 @@ class DialogCloseState {
119
119
  this.#variant = props.variant;
120
120
  this.#disabled = props.disabled;
121
121
  this.onclick = this.onclick.bind(this);
122
- this.onpointerdown = this.onpointerdown.bind(this);
123
122
  this.onkeydown = this.onkeydown.bind(this);
124
123
  useRefById({
125
124
  id: this.#id,
@@ -134,16 +133,6 @@ class DialogCloseState {
134
133
  return;
135
134
  this.#root.handleClose();
136
135
  }
137
- onpointerdown(e) {
138
- if (this.#disabled.current)
139
- return;
140
- if (e.button > 0)
141
- return;
142
- // by default, it will attempt to focus this trigger on pointerdown
143
- // since this also closes the dialog and restores focus we want to prevent that behavior
144
- e.preventDefault();
145
- this.#root.handleClose();
146
- }
147
136
  onkeydown(e) {
148
137
  if (this.#disabled.current)
149
138
  return;
@@ -155,7 +144,6 @@ class DialogCloseState {
155
144
  props = $derived.by(() => ({
156
145
  id: this.#id.current,
157
146
  [this.#attr]: "",
158
- onpointerdown: this.onpointerdown,
159
147
  onclick: this.onclick,
160
148
  onkeydown: this.onkeydown,
161
149
  ...this.#root.sharedProps,
@@ -299,7 +287,6 @@ class AlertDialogCancelState {
299
287
  this.#root = root;
300
288
  this.#disabled = props.disabled;
301
289
  this.onclick = this.onclick.bind(this);
302
- this.onpointerdown = this.onpointerdown.bind(this);
303
290
  this.onkeydown = this.onkeydown.bind(this);
304
291
  useRefById({
305
292
  id: this.#id,
@@ -317,18 +304,6 @@ class AlertDialogCancelState {
317
304
  return;
318
305
  this.#root.handleClose();
319
306
  }
320
- onpointerdown(e) {
321
- if (this.#disabled.current)
322
- return;
323
- if (e.pointerType === "touch")
324
- return e.preventDefault();
325
- if (e.button > 0)
326
- return;
327
- // by default, it will attempt to focus this trigger on pointerdown
328
- // since this also opens the dialog we want to prevent that behavior
329
- e.preventDefault();
330
- this.#root.handleClose();
331
- }
332
307
  onkeydown(e) {
333
308
  if (this.#disabled.current)
334
309
  return;
@@ -340,7 +315,6 @@ class AlertDialogCancelState {
340
315
  props = $derived.by(() => ({
341
316
  id: this.#id.current,
342
317
  [this.#root.attrs.cancel]: "",
343
- onpointerdown: this.onpointerdown,
344
318
  onclick: this.onclick,
345
319
  onkeydown: this.onkeydown,
346
320
  ...this.#root.sharedProps,
@@ -19,6 +19,7 @@
19
19
  onInteractOutside = noop,
20
20
  trapFocus = true,
21
21
  preventScroll = false,
22
+
22
23
  ...restProps
23
24
  }: PopoverContentStaticProps = $props();
24
25
 
@@ -28,28 +29,12 @@
28
29
  () => ref,
29
30
  (v) => (ref = v)
30
31
  ),
32
+ onInteractOutside: box.with(() => onInteractOutside),
33
+ onEscapeKeydown: box.with(() => onEscapeKeydown),
34
+ onCloseAutoFocus: box.with(() => onCloseAutoFocus),
31
35
  });
32
36
 
33
37
  const mergedProps = $derived(mergeProps(restProps, contentState.props));
34
-
35
- function handleInteractOutside(e: PointerEvent) {
36
- onInteractOutside(e);
37
- if (e.defaultPrevented) return;
38
- contentState.root.handleClose();
39
- }
40
-
41
- function handleEscapeKeydown(e: KeyboardEvent) {
42
- onEscapeKeydown(e);
43
- if (e.defaultPrevented) return;
44
- contentState.root.handleClose();
45
- }
46
-
47
- function handleCloseAutoFocus(e: Event) {
48
- onCloseAutoFocus(e);
49
- if (e.defaultPrevented) return;
50
- e.preventDefault();
51
- contentState.root.triggerNode?.focus();
52
- }
53
38
  </script>
54
39
 
55
40
  {#if forceMount}
@@ -58,9 +43,9 @@
58
43
  isStatic
59
44
  enabled={contentState.root.open.current}
60
45
  {id}
61
- onInteractOutside={handleInteractOutside}
62
- onEscapeKeydown={handleEscapeKeydown}
63
- onCloseAutoFocus={handleCloseAutoFocus}
46
+ onInteractOutside={contentState.handleInteractOutside}
47
+ onEscapeKeydown={contentState.handleEscapeKeydown}
48
+ onCloseAutoFocus={contentState.handleCloseAutoFocus}
64
49
  {trapFocus}
65
50
  {preventScroll}
66
51
  loop
@@ -85,9 +70,9 @@
85
70
  isStatic
86
71
  present={contentState.root.open.current}
87
72
  {id}
88
- onInteractOutside={handleInteractOutside}
89
- onEscapeKeydown={handleEscapeKeydown}
90
- onCloseAutoFocus={handleCloseAutoFocus}
73
+ onInteractOutside={contentState.handleInteractOutside}
74
+ onEscapeKeydown={contentState.handleEscapeKeydown}
75
+ onCloseAutoFocus={contentState.handleCloseAutoFocus}
91
76
  {trapFocus}
92
77
  {preventScroll}
93
78
  loop
@@ -7,7 +7,6 @@
7
7
  import { useId } from "../../../internal/use-id.js";
8
8
  import { getFloatingContentCSSVars } from "../../../internal/floating-svelte/floating-utils.svelte.js";
9
9
  import PopperLayerForceMount from "../../utilities/popper-layer/popper-layer-force-mount.svelte";
10
- import { isHTMLElement } from "../../../internal/is.js";
11
10
 
12
11
  let {
13
12
  child,
@@ -29,29 +28,12 @@
29
28
  () => ref,
30
29
  (v) => (ref = v)
31
30
  ),
31
+ onInteractOutside: box.with(() => onInteractOutside),
32
+ onEscapeKeydown: box.with(() => onEscapeKeydown),
33
+ onCloseAutoFocus: box.with(() => onCloseAutoFocus),
32
34
  });
33
35
 
34
36
  const mergedProps = $derived(mergeProps(restProps, contentState.props));
35
-
36
- function handleInteractOutside(e: PointerEvent) {
37
- onInteractOutside(e);
38
- if (e.defaultPrevented) return;
39
- if (isHTMLElement(e.target) && e.target.closest("[data-popover-trigger")) return;
40
- contentState.root.handleClose();
41
- }
42
-
43
- function handleEscapeKeydown(e: KeyboardEvent) {
44
- onEscapeKeydown(e);
45
- if (e.defaultPrevented) return;
46
- contentState.root.handleClose();
47
- }
48
-
49
- function handleCloseAutoFocus(e: Event) {
50
- onCloseAutoFocus(e);
51
- if (e.defaultPrevented) return;
52
- e.preventDefault();
53
- contentState.root.triggerNode?.focus();
54
- }
55
37
  </script>
56
38
 
57
39
  {#if forceMount}
@@ -59,9 +41,9 @@
59
41
  {...mergedProps}
60
42
  enabled={contentState.root.open.current}
61
43
  {id}
62
- onInteractOutside={handleInteractOutside}
63
- onEscapeKeydown={handleEscapeKeydown}
64
- onCloseAutoFocus={handleCloseAutoFocus}
44
+ onInteractOutside={contentState.handleInteractOutside}
45
+ onEscapeKeydown={contentState.handleEscapeKeydown}
46
+ onCloseAutoFocus={contentState.handleCloseAutoFocus}
65
47
  {trapFocus}
66
48
  {preventScroll}
67
49
  loop
@@ -87,9 +69,9 @@
87
69
  {...mergedProps}
88
70
  present={contentState.root.open.current}
89
71
  {id}
90
- onInteractOutside={handleInteractOutside}
91
- onEscapeKeydown={handleEscapeKeydown}
92
- onCloseAutoFocus={handleCloseAutoFocus}
72
+ onInteractOutside={contentState.handleInteractOutside}
73
+ onEscapeKeydown={contentState.handleEscapeKeydown}
74
+ onCloseAutoFocus={contentState.handleCloseAutoFocus}
93
75
  {trapFocus}
94
76
  {preventScroll}
95
77
  loop
@@ -34,11 +34,18 @@ declare class PopoverTriggerState {
34
34
  readonly onclick: (e: BitsMouseEvent) => void;
35
35
  };
36
36
  }
37
- type PopoverContentStateProps = WithRefProps;
37
+ type PopoverContentStateProps = WithRefProps & ReadableBoxedValues<{
38
+ onInteractOutside: (e: PointerEvent) => void;
39
+ onEscapeKeydown: (e: KeyboardEvent) => void;
40
+ onCloseAutoFocus: (e: Event) => void;
41
+ }>;
38
42
  declare class PopoverContentState {
39
43
  #private;
40
44
  root: PopoverRootState;
41
45
  constructor(props: PopoverContentStateProps, root: PopoverRootState);
46
+ handleInteractOutside(e: PointerEvent): void;
47
+ handleEscapeKeydown(e: KeyboardEvent): void;
48
+ handleCloseAutoFocus(e: Event): void;
42
49
  snippetProps: {
43
50
  open: boolean;
44
51
  };
@@ -2,6 +2,7 @@ import { useRefById } from "svelte-toolbelt";
2
2
  import { Context } from "runed";
3
3
  import { kbd } from "../../internal/kbd.js";
4
4
  import { getAriaExpanded, getDataOpenClosed } from "../../internal/attrs.js";
5
+ import { isElement } from "../../internal/is.js";
5
6
  class PopoverRootState {
6
7
  open;
7
8
  contentNode = $state(null);
@@ -87,10 +88,16 @@ class PopoverContentState {
87
88
  #id;
88
89
  #ref;
89
90
  root;
91
+ #onInteractOutside;
92
+ #onEscapeKeydown;
93
+ #onCloseAutoFocus;
90
94
  constructor(props, root) {
91
95
  this.#id = props.id;
92
96
  this.root = root;
93
97
  this.#ref = props.ref;
98
+ this.#onEscapeKeydown = props.onEscapeKeydown;
99
+ this.#onInteractOutside = props.onInteractOutside;
100
+ this.#onCloseAutoFocus = props.onCloseAutoFocus;
94
101
  useRefById({
95
102
  id: this.#id,
96
103
  ref: this.#ref,
@@ -99,6 +106,33 @@ class PopoverContentState {
99
106
  this.root.contentNode = node;
100
107
  },
101
108
  });
109
+ this.handleInteractOutside = this.handleInteractOutside.bind(this);
110
+ this.handleEscapeKeydown = this.handleEscapeKeydown.bind(this);
111
+ this.handleCloseAutoFocus = this.handleCloseAutoFocus.bind(this);
112
+ }
113
+ handleInteractOutside(e) {
114
+ this.#onInteractOutside.current(e);
115
+ if (e.defaultPrevented)
116
+ return;
117
+ if (!isElement(e.target))
118
+ return;
119
+ const closestTrigger = e.target.closest(`[data-popover-trigger]`);
120
+ if (closestTrigger === this.root.triggerNode)
121
+ return;
122
+ this.root.handleClose();
123
+ }
124
+ handleEscapeKeydown(e) {
125
+ this.#onEscapeKeydown.current(e);
126
+ if (e.defaultPrevented)
127
+ return;
128
+ this.root.handleClose();
129
+ }
130
+ handleCloseAutoFocus(e) {
131
+ this.#onCloseAutoFocus.current(e);
132
+ if (e.defaultPrevented)
133
+ return;
134
+ e.preventDefault();
135
+ this.root.triggerNode?.focus();
102
136
  }
103
137
  snippetProps = $derived.by(() => ({ open: this.root.open.current }));
104
138
  props = $derived.by(() => ({
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { box, mergeProps } from "svelte-toolbelt";
2
+ import { box, mergeProps, type WritableBox } from "svelte-toolbelt";
3
3
  import type { SliderRootProps } from "../types.js";
4
4
  import { useSliderRoot } from "../slider.svelte.js";
5
5
  import { useId } from "../../../internal/use-id.js";
@@ -10,7 +10,8 @@
10
10
  child,
11
11
  id = useId(),
12
12
  ref = $bindable(null),
13
- value = $bindable([]),
13
+ value = $bindable(),
14
+ type,
14
15
  onValueChange = noop,
15
16
  onValueCommit = noop,
16
17
  disabled = false,
@@ -24,6 +25,10 @@
24
25
  ...restProps
25
26
  }: SliderRootProps = $props();
26
27
 
28
+ if (value === undefined) {
29
+ value = type === "single" ? 0 : [];
30
+ }
31
+
27
32
  const rootState = useSliderRoot({
28
33
  id: box.with(() => id),
29
34
  ref: box.with(
@@ -34,13 +39,16 @@
34
39
  () => value,
35
40
  (v) => {
36
41
  if (controlledValue) {
42
+ // @ts-expect-error - we know
37
43
  onValueChange(v);
38
44
  } else {
39
45
  value = v;
46
+ // @ts-expect-error - we know
40
47
  onValueChange(v);
41
48
  }
42
49
  }
43
- ),
50
+ ) as WritableBox<number> | WritableBox<number[]>,
51
+ // @ts-expect-error - we know
44
52
  onValueCommit: box.with(() => onValueCommit),
45
53
  disabled: box.with(() => disabled),
46
54
  min: box.with(() => min),
@@ -49,6 +57,7 @@
49
57
  dir: box.with(() => dir),
50
58
  autoSort: box.with(() => autoSort),
51
59
  orientation: box.with(() => orientation),
60
+ type,
52
61
  });
53
62
 
54
63
  const mergedProps = $derived(mergeProps(restProps, rootState.props));
@@ -1,7 +1,8 @@
1
+ import { type Box, type ReadableBox } from "svelte-toolbelt";
1
2
  import type { ReadableBoxedValues, WritableBoxedValues } from "../../internal/box.svelte.js";
2
3
  import type { BitsKeyboardEvent, OnChangeFn, WithRefProps } from "../../internal/types.js";
3
4
  import type { Direction, Orientation } from "../../shared/index.js";
4
- type SliderRootStateProps = WithRefProps<ReadableBoxedValues<{
5
+ type SliderBaseRootStateProps = WithRefProps<ReadableBoxedValues<{
5
6
  disabled: boolean;
6
7
  orientation: Orientation;
7
8
  min: number;
@@ -9,31 +10,95 @@ type SliderRootStateProps = WithRefProps<ReadableBoxedValues<{
9
10
  step: number;
10
11
  dir: Direction;
11
12
  autoSort: boolean;
13
+ }>>;
14
+ declare class SliderBaseRootState {
15
+ #private;
16
+ id: SliderBaseRootStateProps["id"];
17
+ ref: SliderBaseRootStateProps["ref"];
18
+ disabled: SliderBaseRootStateProps["disabled"];
19
+ orientation: SliderBaseRootStateProps["orientation"];
20
+ min: SliderBaseRootStateProps["min"];
21
+ max: SliderBaseRootStateProps["max"];
22
+ step: SliderBaseRootStateProps["step"];
23
+ dir: SliderBaseRootStateProps["dir"];
24
+ autoSort: SliderBaseRootStateProps["autoSort"];
25
+ isActive: boolean;
26
+ direction: "rl" | "lr" | "tb" | "bt";
27
+ constructor(props: SliderBaseRootStateProps);
28
+ getAllThumbs: () => HTMLElement[];
29
+ props: {
30
+ readonly id: string;
31
+ readonly "data-orientation": "horizontal" | "vertical";
32
+ readonly "data-disabled": "" | undefined;
33
+ readonly style: {
34
+ readonly touchAction: string | undefined;
35
+ };
36
+ readonly "data-slider-root": "";
37
+ };
38
+ }
39
+ type SliderSingleRootStateProps = SliderBaseRootStateProps & ReadableBoxedValues<{
40
+ onValueCommit: OnChangeFn<number>;
41
+ }> & WritableBoxedValues<{
42
+ value: number;
43
+ }>;
44
+ declare class SliderSingleRootState extends SliderBaseRootState {
45
+ value: SliderSingleRootStateProps["value"];
46
+ onValueCommit: SliderSingleRootStateProps["onValueCommit"];
47
+ isMulti: false;
48
+ constructor(props: SliderSingleRootStateProps);
49
+ applyPosition({ clientXY, start, end }: {
50
+ clientXY: number;
51
+ start: number;
52
+ end: number;
53
+ }): void;
54
+ updateValue: (newValue: number) => void;
55
+ handlePointerMove: (e: PointerEvent) => void;
56
+ handlePointerDown: (e: PointerEvent) => void;
57
+ handlePointerUp: () => void;
58
+ getPositionFromValue: (thumbValue: number) => number;
59
+ thumbsPropsArr: {
60
+ readonly role: "slider";
61
+ readonly "aria-valuemin": number;
62
+ readonly "aria-valuemax": number;
63
+ readonly "aria-valuenow": number;
64
+ readonly "aria-disabled": "true" | "false";
65
+ readonly "aria-orientation": "horizontal" | "vertical";
66
+ readonly "data-value": number;
67
+ readonly tabindex: 0 | -1;
68
+ readonly style: import("../../shared/index.js").StyleProperties;
69
+ readonly "data-slider-thumb": "";
70
+ }[];
71
+ thumbsRenderArr: number[];
72
+ ticksPropsArr: {
73
+ readonly "data-disabled": "" | undefined;
74
+ readonly "data-orientation": "horizontal" | "vertical";
75
+ readonly "data-bounded": "" | undefined;
76
+ readonly "data-value": number;
77
+ readonly style: import("../../shared/index.js").StyleProperties;
78
+ readonly "data-slider-tick": "";
79
+ }[];
80
+ ticksRenderArr: number[];
81
+ snippetProps: {
82
+ readonly ticks: number[];
83
+ readonly thumbs: number[];
84
+ };
85
+ }
86
+ type SliderMultiRootStateProps = SliderBaseRootStateProps & ReadableBoxedValues<{
12
87
  onValueCommit: OnChangeFn<number[]>;
13
88
  }> & WritableBoxedValues<{
14
89
  value: number[];
15
- }>>;
16
- declare class SliderRootState {
90
+ }>;
91
+ declare class SliderMultiRootState extends SliderBaseRootState {
17
92
  #private;
18
- id: SliderRootStateProps["id"];
19
- ref: SliderRootStateProps["ref"];
20
- value: SliderRootStateProps["value"];
21
- disabled: SliderRootStateProps["disabled"];
22
- orientation: SliderRootStateProps["orientation"];
23
- min: SliderRootStateProps["min"];
24
- max: SliderRootStateProps["max"];
25
- step: SliderRootStateProps["step"];
26
- dir: SliderRootStateProps["dir"];
27
- autoSort: SliderRootStateProps["autoSort"];
93
+ value: SliderMultiRootStateProps["value"];
94
+ isMulti: true;
28
95
  activeThumb: {
29
96
  node: HTMLElement;
30
97
  idx: number;
31
98
  } | null;
32
- isActive: boolean;
33
99
  currentThumbIdx: number;
34
- direction: "rl" | "lr" | "tb" | "bt";
35
- onValueCommit: SliderRootStateProps["onValueCommit"];
36
- constructor(props: SliderRootStateProps);
100
+ onValueCommit: SliderMultiRootStateProps["onValueCommit"];
101
+ constructor(props: SliderMultiRootStateProps);
37
102
  applyPosition({ clientXY, activeThumbIdx, start, end, }: {
38
103
  clientXY: number;
39
104
  activeThumbIdx: number;
@@ -72,15 +137,6 @@ declare class SliderRootState {
72
137
  readonly ticks: number[];
73
138
  readonly thumbs: number[];
74
139
  };
75
- props: {
76
- readonly id: string;
77
- readonly "data-orientation": "horizontal" | "vertical";
78
- readonly "data-disabled": "" | undefined;
79
- readonly style: {
80
- readonly touchAction: string | undefined;
81
- };
82
- readonly "data-slider-root": "";
83
- };
84
140
  }
85
141
  type SliderRangeStateProps = WithRefProps;
86
142
  declare class SliderRangeState {
@@ -1756,6 +1812,19 @@ declare class SliderThumbState {
1756
1812
  constructor(props: SliderThumbStateProps, root: SliderRootState);
1757
1813
  onkeydown(e: BitsKeyboardEvent): void;
1758
1814
  props: {
1815
+ readonly id: string;
1816
+ readonly onkeydown: (e: BitsKeyboardEvent) => void;
1817
+ readonly role: "slider";
1818
+ readonly "aria-valuemin": number;
1819
+ readonly "aria-valuemax": number;
1820
+ readonly "aria-valuenow": number;
1821
+ readonly "aria-disabled": "true" | "false";
1822
+ readonly "aria-orientation": "horizontal" | "vertical";
1823
+ readonly "data-value": number;
1824
+ readonly tabindex: 0 | -1;
1825
+ readonly style: import("../../shared/index.js").StyleProperties;
1826
+ readonly "data-slider-thumb": "";
1827
+ } | {
1759
1828
  readonly id: string;
1760
1829
  readonly onkeydown: (e: BitsKeyboardEvent) => void;
1761
1830
  readonly role: "slider";
@@ -1784,9 +1853,23 @@ declare class SliderTickState {
1784
1853
  readonly "data-value": number;
1785
1854
  readonly style: import("../../shared/index.js").StyleProperties;
1786
1855
  readonly "data-slider-tick": "";
1856
+ } | {
1857
+ readonly id: string;
1858
+ readonly "data-disabled": "" | undefined;
1859
+ readonly "data-orientation": "horizontal" | "vertical";
1860
+ readonly "data-bounded": "" | undefined;
1861
+ readonly "data-value": number;
1862
+ readonly style: import("../../shared/index.js").StyleProperties;
1863
+ readonly "data-slider-tick": "";
1787
1864
  };
1788
1865
  }
1789
- export declare function useSliderRoot(props: SliderRootStateProps): SliderRootState;
1866
+ type SliderRootState = SliderSingleRootState | SliderMultiRootState;
1867
+ type InitSliderRootStateProps = {
1868
+ type: "single" | "multiple";
1869
+ value: Box<number> | Box<number[]>;
1870
+ onValueCommit: ReadableBox<OnChangeFn<number>> | ReadableBox<OnChangeFn<number[]>>;
1871
+ } & Omit<SliderBaseRootStateProps, "type">;
1872
+ export declare function useSliderRoot(props: InitSliderRootStateProps): SliderRootState;
1790
1873
  export declare function useSliderRange(props: SliderRangeStateProps): SliderRangeState;
1791
1874
  export declare function useSliderThumb(props: SliderThumbStateProps): SliderThumbState;
1792
1875
  export declare function useSliderTick(props: SliderTickStateProps): SliderTickState;
@@ -3,9 +3,9 @@
3
3
  * Abdelrahman (https://github.com/abdel-17)
4
4
  */
5
5
  import { untrack } from "svelte";
6
- import { executeCallbacks, useRefById } from "svelte-toolbelt";
6
+ import { executeCallbacks, onMountEffect, useRefById, } from "svelte-toolbelt";
7
7
  import { on } from "svelte/events";
8
- import { Context } from "runed";
8
+ import { Context, watch } from "runed";
9
9
  import { getRangeStyles, getThumbStyles, getTickStyles } from "./helpers.js";
10
10
  import { getAriaDisabled, getAriaOrientation, getDataDisabled, getDataOrientation, } from "../../internal/attrs.js";
11
11
  import { kbd } from "../../internal/kbd.js";
@@ -16,10 +16,9 @@ const SLIDER_ROOT_ATTR = "data-slider-root";
16
16
  const SLIDER_THUMB_ATTR = "data-slider-thumb";
17
17
  const SLIDER_RANGE_ATTR = "data-slider-range";
18
18
  const SLIDER_TICK_ATTR = "data-slider-tick";
19
- class SliderRootState {
19
+ class SliderBaseRootState {
20
20
  id;
21
21
  ref;
22
- value;
23
22
  disabled;
24
23
  orientation;
25
24
  min;
@@ -27,9 +26,7 @@ class SliderRootState {
27
26
  step;
28
27
  dir;
29
28
  autoSort;
30
- activeThumb = $state(null);
31
29
  isActive = $state(false);
32
- currentThumbIdx = $state(0);
33
30
  direction = $derived.by(() => {
34
31
  if (this.orientation.current === "horizontal") {
35
32
  return this.dir.current === "rtl" ? "rl" : "lr";
@@ -38,7 +35,6 @@ class SliderRootState {
38
35
  return this.dir.current === "rtl" ? "tb" : "bt";
39
36
  }
40
37
  });
41
- onValueCommit;
42
38
  constructor(props) {
43
39
  this.id = props.id;
44
40
  this.ref = props.ref;
@@ -49,20 +45,234 @@ class SliderRootState {
49
45
  this.step = props.step;
50
46
  this.dir = props.dir;
51
47
  this.autoSort = props.autoSort;
52
- this.value = props.value;
53
- this.onValueCommit = props.onValueCommit;
54
48
  useRefById({
55
49
  id: this.id,
56
50
  ref: this.ref,
57
51
  });
58
- $effect(() => {
52
+ }
53
+ #touchAction = $derived.by(() => {
54
+ if (this.disabled.current)
55
+ return undefined;
56
+ return this.orientation.current === "horizontal" ? "pan-y" : "pan-x";
57
+ });
58
+ getAllThumbs = () => {
59
+ const node = this.ref.current;
60
+ if (!node)
61
+ return [];
62
+ return Array.from(node.querySelectorAll(`[${SLIDER_THUMB_ATTR}]`));
63
+ };
64
+ props = $derived.by(() => ({
65
+ id: this.id.current,
66
+ "data-orientation": getDataOrientation(this.orientation.current),
67
+ "data-disabled": getDataDisabled(this.disabled.current),
68
+ style: {
69
+ touchAction: this.#touchAction,
70
+ },
71
+ [SLIDER_ROOT_ATTR]: "",
72
+ }));
73
+ }
74
+ class SliderSingleRootState extends SliderBaseRootState {
75
+ value;
76
+ onValueCommit;
77
+ isMulti = false;
78
+ constructor(props) {
79
+ super(props);
80
+ this.value = props.value;
81
+ this.onValueCommit = props.onValueCommit;
82
+ onMountEffect(() => {
59
83
  return executeCallbacks(on(document, "pointerdown", this.handlePointerDown), on(document, "pointerup", this.handlePointerUp), on(document, "pointermove", this.handlePointerMove), on(document, "pointerleave", this.handlePointerUp));
60
84
  });
61
- $effect(() => {
85
+ watch([
86
+ () => this.step.current,
87
+ () => this.min.current,
88
+ () => this.max.current,
89
+ () => this.value.current,
90
+ ], ([step, min, max, value]) => {
91
+ const isValidValue = (v) => {
92
+ const snappedValue = snapValueToStep(v, min, max, step);
93
+ return snappedValue === v;
94
+ };
95
+ const gcv = (v) => {
96
+ return snapValueToStep(v, min, max, step);
97
+ };
98
+ if (!isValidValue(value)) {
99
+ this.value.current = gcv(value);
100
+ }
101
+ });
102
+ }
103
+ applyPosition({ clientXY, start, end }) {
104
+ const min = this.min.current;
105
+ const max = this.max.current;
106
+ const percent = (clientXY - start) / (end - start);
107
+ const val = percent * (max - min) + min;
108
+ if (val < min) {
109
+ this.updateValue(min);
110
+ }
111
+ else if (val > max) {
112
+ this.updateValue(max);
113
+ }
114
+ else {
62
115
  const step = this.step.current;
63
- const min = this.min.current;
64
- const max = this.max.current;
65
- const value = this.value.current;
116
+ const currStep = Math.floor((val - min) / step);
117
+ const midpointOfCurrStep = min + currStep * step + step / 2;
118
+ const midpointOfNextStep = min + (currStep + 1) * step + step / 2;
119
+ const newValue = val >= midpointOfCurrStep && val < midpointOfNextStep
120
+ ? (currStep + 1) * step + min
121
+ : currStep * step + min;
122
+ if (newValue <= max) {
123
+ this.updateValue(newValue);
124
+ }
125
+ }
126
+ }
127
+ updateValue = (newValue) => {
128
+ this.value.current = snapValueToStep(newValue, this.min.current, this.max.current, this.step.current);
129
+ };
130
+ handlePointerMove = (e) => {
131
+ if (!this.isActive || this.disabled.current)
132
+ return;
133
+ e.preventDefault();
134
+ e.stopPropagation();
135
+ const sliderNode = this.ref.current;
136
+ const activeThumb = this.getAllThumbs()[0];
137
+ if (!sliderNode || !activeThumb)
138
+ return;
139
+ activeThumb.focus();
140
+ const { left, right, top, bottom } = sliderNode.getBoundingClientRect();
141
+ if (this.direction === "lr") {
142
+ this.applyPosition({
143
+ clientXY: e.clientX,
144
+ start: left,
145
+ end: right,
146
+ });
147
+ }
148
+ else if (this.direction === "rl") {
149
+ this.applyPosition({
150
+ clientXY: e.clientX,
151
+ start: right,
152
+ end: left,
153
+ });
154
+ }
155
+ else if (this.direction === "bt") {
156
+ this.applyPosition({
157
+ clientXY: e.clientY,
158
+ start: bottom,
159
+ end: top,
160
+ });
161
+ }
162
+ else if (this.direction === "tb") {
163
+ this.applyPosition({
164
+ clientXY: e.clientY,
165
+ start: top,
166
+ end: bottom,
167
+ });
168
+ }
169
+ };
170
+ handlePointerDown = (e) => {
171
+ if (e.button !== 0 || this.disabled.current)
172
+ return;
173
+ const sliderNode = this.ref.current;
174
+ const closestThumb = this.getAllThumbs()[0];
175
+ if (!closestThumb || !sliderNode)
176
+ return;
177
+ const target = e.target;
178
+ if (!isElementOrSVGElement(target) || !sliderNode.contains(target))
179
+ return;
180
+ e.preventDefault();
181
+ closestThumb.focus();
182
+ this.isActive = true;
183
+ this.handlePointerMove(e);
184
+ };
185
+ handlePointerUp = () => {
186
+ if (this.disabled.current)
187
+ return;
188
+ if (this.isActive) {
189
+ this.onValueCommit.current(untrack(() => this.value.current));
190
+ }
191
+ this.isActive = false;
192
+ };
193
+ getPositionFromValue = (thumbValue) => {
194
+ const min = this.min.current;
195
+ const max = this.max.current;
196
+ return ((thumbValue - min) / (max - min)) * 100;
197
+ };
198
+ thumbsPropsArr = $derived.by(() => {
199
+ const currValue = this.value.current;
200
+ return Array.from({ length: 1 }, () => {
201
+ const thumbValue = currValue;
202
+ const thumbPosition = this.getPositionFromValue(thumbValue ?? 0);
203
+ const style = getThumbStyles(this.direction, thumbPosition);
204
+ return {
205
+ role: "slider",
206
+ "aria-valuemin": this.min.current,
207
+ "aria-valuemax": this.max.current,
208
+ "aria-valuenow": thumbValue,
209
+ "aria-disabled": getAriaDisabled(this.disabled.current),
210
+ "aria-orientation": getAriaOrientation(this.orientation.current),
211
+ "data-value": thumbValue,
212
+ tabindex: this.disabled.current ? -1 : 0,
213
+ style,
214
+ [SLIDER_THUMB_ATTR]: "",
215
+ };
216
+ });
217
+ });
218
+ thumbsRenderArr = $derived.by(() => {
219
+ return this.thumbsPropsArr.map((_, i) => i);
220
+ });
221
+ ticksPropsArr = $derived.by(() => {
222
+ const max = this.max.current;
223
+ const min = this.min.current;
224
+ const step = this.step.current;
225
+ const difference = max - min;
226
+ let count = Math.ceil(difference / step);
227
+ if (difference % step == 0) {
228
+ count++;
229
+ }
230
+ const currValue = this.value.current;
231
+ return Array.from({ length: count }, (_, i) => {
232
+ const tickPosition = i * (step / difference) * 100;
233
+ const isFirst = i === 0;
234
+ const isLast = i === count - 1;
235
+ const offsetPercentage = isFirst ? 0 : isLast ? -100 : -50;
236
+ const style = getTickStyles(this.direction, tickPosition, offsetPercentage);
237
+ const tickValue = min + i * step;
238
+ const bounded = tickValue <= currValue;
239
+ return {
240
+ "data-disabled": getDataDisabled(this.disabled.current),
241
+ "data-orientation": getDataOrientation(this.orientation.current),
242
+ "data-bounded": bounded ? "" : undefined,
243
+ "data-value": tickValue,
244
+ style,
245
+ [SLIDER_TICK_ATTR]: "",
246
+ };
247
+ });
248
+ });
249
+ ticksRenderArr = $derived.by(() => {
250
+ return this.ticksPropsArr.map((_, i) => i);
251
+ });
252
+ snippetProps = $derived.by(() => ({
253
+ ticks: this.ticksRenderArr,
254
+ thumbs: this.thumbsRenderArr,
255
+ }));
256
+ }
257
+ class SliderMultiRootState extends SliderBaseRootState {
258
+ value;
259
+ isMulti = true;
260
+ activeThumb = $state(null);
261
+ currentThumbIdx = $state(0);
262
+ onValueCommit;
263
+ constructor(props) {
264
+ super(props);
265
+ this.value = props.value;
266
+ this.onValueCommit = props.onValueCommit;
267
+ onMountEffect(() => {
268
+ return executeCallbacks(on(document, "pointerdown", this.handlePointerDown), on(document, "pointerup", this.handlePointerUp), on(document, "pointermove", this.handlePointerMove), on(document, "pointerleave", this.handlePointerUp));
269
+ });
270
+ watch([
271
+ () => this.step.current,
272
+ () => this.min.current,
273
+ () => this.max.current,
274
+ () => this.value.current,
275
+ ], ([step, min, max, value]) => {
66
276
  const isValidValue = (v) => {
67
277
  const snappedValue = snapValueToStep(v, min, max, step);
68
278
  return snappedValue === v;
@@ -306,20 +516,6 @@ class SliderRootState {
306
516
  ticks: this.ticksRenderArr,
307
517
  thumbs: this.thumbsRenderArr,
308
518
  }));
309
- #touchAction = $derived.by(() => {
310
- if (this.disabled.current)
311
- return undefined;
312
- return this.orientation.current === "horizontal" ? "pan-y" : "pan-x";
313
- });
314
- props = $derived.by(() => ({
315
- id: this.id.current,
316
- "data-orientation": getDataOrientation(this.orientation.current),
317
- "data-disabled": getDataDisabled(this.disabled.current),
318
- style: {
319
- touchAction: this.#touchAction,
320
- },
321
- [SLIDER_ROOT_ATTR]: "",
322
- }));
323
519
  }
324
520
  const VALID_SLIDER_KEYS = [
325
521
  kbd.ARROW_LEFT,
@@ -343,9 +539,14 @@ class SliderRangeState {
343
539
  });
344
540
  }
345
541
  rangeStyles = $derived.by(() => {
346
- const value = this.#root.value.current;
347
- const min = value.length > 1 ? this.#root.getPositionFromValue(Math.min(...value) ?? 0) : 0;
348
- const max = 100 - this.#root.getPositionFromValue(Math.max(...value) ?? 0);
542
+ const min = Array.isArray(this.#root.value.current)
543
+ ? this.#root.value.current.length > 1
544
+ ? this.#root.getPositionFromValue(Math.min(...this.#root.value.current) ?? 0)
545
+ : 0
546
+ : 0;
547
+ const max = Array.isArray(this.#root.value.current)
548
+ ? 100 - this.#root.getPositionFromValue(Math.max(...this.#root.value.current) ?? 0)
549
+ : 100 - this.#root.getPositionFromValue(this.#root.value.current);
349
550
  return {
350
551
  position: "absolute",
351
552
  ...getRangeStyles(this.#root.direction, min, max),
@@ -377,7 +578,12 @@ class SliderThumbState {
377
578
  this.onkeydown = this.onkeydown.bind(this);
378
579
  }
379
580
  #updateValue(newValue) {
380
- this.#root.updateValue(newValue, this.#index.current);
581
+ if (this.#root.isMulti) {
582
+ this.#root.updateValue(newValue, this.#index.current);
583
+ }
584
+ else {
585
+ this.#root.updateValue(newValue);
586
+ }
381
587
  }
382
588
  onkeydown(e) {
383
589
  if (this.#isDisabled)
@@ -389,14 +595,16 @@ class SliderThumbState {
389
595
  if (!thumbs.length)
390
596
  return;
391
597
  const idx = thumbs.indexOf(currNode);
392
- this.#root.currentThumbIdx = idx;
598
+ if (this.#root.isMulti) {
599
+ this.#root.currentThumbIdx = idx;
600
+ }
393
601
  if (!VALID_SLIDER_KEYS.includes(e.key))
394
602
  return;
395
603
  e.preventDefault();
396
604
  const min = this.#root.min.current;
397
605
  const max = this.#root.max.current;
398
606
  const value = this.#root.value.current;
399
- const thumbValue = value[idx];
607
+ const thumbValue = Array.isArray(value) ? value[idx] : value;
400
608
  const orientation = this.#root.orientation.current;
401
609
  const direction = this.#root.direction;
402
610
  const step = this.#root.step.current;
@@ -460,6 +668,7 @@ class SliderThumbState {
460
668
  }
461
669
  break;
462
670
  }
671
+ // @ts-expect-error - this is fine
463
672
  this.#root.onValueCommit.current(this.#root.value.current);
464
673
  }
465
674
  props = $derived.by(() => ({
@@ -490,7 +699,11 @@ class SliderTickState {
490
699
  }
491
700
  const SliderRootContext = new Context("Slider.Root");
492
701
  export function useSliderRoot(props) {
493
- return SliderRootContext.set(new SliderRootState(props));
702
+ const { type, ...rest } = props;
703
+ const rootState = type === "single"
704
+ ? new SliderSingleRootState(rest)
705
+ : new SliderMultiRootState(rest);
706
+ return SliderRootContext.set(rootState);
494
707
  }
495
708
  export function useSliderRange(props) {
496
709
  return new SliderRangeState(props, SliderRootContext.get());
@@ -5,22 +5,7 @@ export type SliderRootSnippetProps = {
5
5
  ticks: number[];
6
6
  thumbs: number[];
7
7
  };
8
- export type SliderRootPropsWithoutHTML = WithChild<{
9
- /**
10
- * The value of the slider.
11
- * @bindable
12
- */
13
- value?: number[];
14
- /**
15
- * A callback function called when the value changes.
16
- */
17
- onValueChange?: OnChangeFn<number[]>;
18
- /**
19
- * A callback function called when the user stops dragging the thumb,
20
- * which is useful for knowing when the user has finished interacting with the
21
- * slider and _commits_ the value.
22
- */
23
- onValueCommit?: OnChangeFn<number[]>;
8
+ export type BaseSliderRootPropsWithoutHTML = {
24
9
  /**
25
10
  * Whether to automatically sort the values in the array when moving thumbs past
26
11
  * one another.
@@ -43,7 +28,7 @@ export type SliderRootPropsWithoutHTML = WithChild<{
43
28
  /**
44
29
  * The amount to increment the value by when the user presses the arrow keys.
45
30
  *
46
- * @defayltValue 1
31
+ * @defaultValue 1
47
32
  */
48
33
  step?: number;
49
34
  /**
@@ -77,7 +62,56 @@ export type SliderRootPropsWithoutHTML = WithChild<{
77
62
  * @defaultValue false
78
63
  */
79
64
  controlledValue?: boolean;
80
- }, SliderRootSnippetProps>;
65
+ };
66
+ export type SliderSingleRootPropsWithoutHTML = BaseSliderRootPropsWithoutHTML & {
67
+ /**
68
+ * The type of slider. If set to `'multiple'`, the slider will
69
+ * allow multiple ticks and the `value` will be an array of numbers.
70
+ *
71
+ * @required
72
+ */
73
+ type: "single";
74
+ /**
75
+ * The value of the slider.
76
+ * @bindable
77
+ */
78
+ value?: number;
79
+ /**
80
+ * A callback function called when the value changes.
81
+ */
82
+ onValueChange?: OnChangeFn<number>;
83
+ /**
84
+ * A callback function called when the user stops dragging the
85
+ * thumb and the value is committed.
86
+ */
87
+ onValueCommit?: OnChangeFn<number>;
88
+ };
89
+ export type SliderMultiRootPropsWithoutHTML = BaseSliderRootPropsWithoutHTML & {
90
+ /**
91
+ * The type of slider. If set to `'multiple'`, the slider will
92
+ * allow multiple ticks and the `value` will be an array of numbers.
93
+ *
94
+ * @required
95
+ */
96
+ type: "multiple";
97
+ /**
98
+ * The value of the slider.
99
+ * @bindable
100
+ */
101
+ value?: number[];
102
+ /**
103
+ * A callback function called when the value changes.
104
+ */
105
+ onValueChange?: OnChangeFn<number[]>;
106
+ /**
107
+ * A callback function called when the user stops dragging the
108
+ * thumb and the value is committed.
109
+ */
110
+ onValueCommit?: OnChangeFn<number[]>;
111
+ };
112
+ export type SliderRootPropsWithoutHTML = WithChild<SliderSingleRootPropsWithoutHTML, SliderRootSnippetProps> | WithChild<SliderMultiRootPropsWithoutHTML, SliderRootSnippetProps>;
113
+ export type SliderSingleRootProps = SliderSingleRootPropsWithoutHTML & Without<BitsPrimitiveSpanAttributes, WithChild<SliderSingleRootPropsWithoutHTML, SliderRootSnippetProps>>;
114
+ export type SliderMultipleRootProps = SliderMultiRootPropsWithoutHTML & Without<BitsPrimitiveSpanAttributes, WithChild<SliderMultiRootPropsWithoutHTML, SliderRootSnippetProps>>;
81
115
  export type SliderRootProps = SliderRootPropsWithoutHTML & Without<BitsPrimitiveSpanAttributes, SliderRootPropsWithoutHTML>;
82
116
  export type SliderRangePropsWithoutHTML = WithChild;
83
117
  export type SliderRangeProps = SliderRangePropsWithoutHTML & Without<BitsPrimitiveSpanAttributes, SliderRangePropsWithoutHTML>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "1.0.0-next.75",
3
+ "version": "1.0.0-next.76",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",