bits-ui 1.0.0-next.79 → 1.0.0-next.80

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.
@@ -35,4 +35,5 @@ export { Toggle } from "./toggle/index.js";
35
35
  export { ToggleGroup } from "./toggle-group/index.js";
36
36
  export { Toolbar } from "./toolbar/index.js";
37
37
  export { Tooltip } from "./tooltip/index.js";
38
+ export { IsUsingKeyboard } from "./utilities/is-using-keyboard/is-using-keyboard.svelte.js";
38
39
  export { default as Portal } from "./utilities/portal/portal.svelte";
@@ -35,4 +35,5 @@ export { Toggle } from "./toggle/index.js";
35
35
  export { ToggleGroup } from "./toggle-group/index.js";
36
36
  export { Toolbar } from "./toolbar/index.js";
37
37
  export { Tooltip } from "./tooltip/index.js";
38
+ export { IsUsingKeyboard } from "./utilities/is-using-keyboard/is-using-keyboard.svelte.js";
38
39
  export { default as Portal } from "./utilities/portal/portal.svelte";
@@ -5,6 +5,7 @@ import { CustomEventDispatcher } from "../../internal/events.js";
5
5
  import type { AnyFn, BitsFocusEvent, BitsKeyboardEvent, BitsMouseEvent, BitsPointerEvent, WithRefProps } from "../../internal/types.js";
6
6
  import { useRovingFocus } from "../../internal/use-roving-focus.svelte.js";
7
7
  import type { Direction } from "../../shared/index.js";
8
+ import { IsUsingKeyboard } from "../../index.js";
8
9
  export declare const CONTEXT_MENU_TRIGGER_ATTR = "data-context-menu-trigger";
9
10
  type MenuVariant = "context-menu" | "dropdown-menu" | "menubar";
10
11
  export type MenuRootStateProps = ReadableBoxedValues<{
@@ -17,7 +18,7 @@ export declare const MenuOpenEvent: CustomEventDispatcher<unknown>;
17
18
  declare class MenuRootState {
18
19
  onClose: MenuRootStateProps["onClose"];
19
20
  variant: MenuRootStateProps["variant"];
20
- isUsingKeyboard: boolean;
21
+ isUsingKeyboard: IsUsingKeyboard;
21
22
  dir: MenuRootStateProps["dir"];
22
23
  constructor(props: MenuRootStateProps);
23
24
  getAttr(name: string): string;
@@ -1,6 +1,5 @@
1
- import { afterTick, box, executeCallbacks, mergeProps, onDestroyEffect, useRefById, } from "svelte-toolbelt";
1
+ import { afterTick, box, mergeProps, onDestroyEffect, useRefById } from "svelte-toolbelt";
2
2
  import { Context, IsFocusWithin, watch } from "runed";
3
- import { on } from "svelte/events";
4
3
  import { FIRST_LAST_KEYS, LAST_KEYS, SELECTION_KEYS, SUB_OPEN_KEYS, getCheckedState, isMouseEvent, } from "./utils.js";
5
4
  import { focusFirst } from "../../internal/focus.js";
6
5
  import { CustomEventDispatcher } from "../../internal/events.js";
@@ -10,6 +9,7 @@ import { useRovingFocus } from "../../internal/use-roving-focus.svelte.js";
10
9
  import { kbd } from "../../internal/kbd.js";
11
10
  import { getAriaChecked, getAriaDisabled, getAriaExpanded, getAriaOrientation, getDataDisabled, getDataOpenClosed, } from "../../internal/attrs.js";
12
11
  import { isPointerInGraceArea, makeHullFromElements } from "../../internal/polygon.js";
12
+ import { IsUsingKeyboard } from "../../index.js";
13
13
  export const CONTEXT_MENU_TRIGGER_ATTR = "data-context-menu-trigger";
14
14
  const MenuRootContext = new Context("Menu.Root");
15
15
  const MenuMenuContext = new Context("Menu.Root | Menu.Sub");
@@ -23,37 +23,12 @@ export const MenuOpenEvent = new CustomEventDispatcher("bitsmenuopen", {
23
23
  class MenuRootState {
24
24
  onClose;
25
25
  variant;
26
- isUsingKeyboard = $state(false);
26
+ isUsingKeyboard = new IsUsingKeyboard();
27
27
  dir;
28
28
  constructor(props) {
29
29
  this.onClose = props.onClose;
30
30
  this.dir = props.dir;
31
31
  this.variant = props.variant;
32
- $effect(() => {
33
- const callbacksToDispose = [];
34
- const handlePointer = (_) => {
35
- this.isUsingKeyboard = false;
36
- };
37
- const handleKeydown = (_) => {
38
- this.isUsingKeyboard = true;
39
- const disposePointerDown = on(document, "pointerdown", handlePointer, {
40
- capture: true,
41
- once: true,
42
- });
43
- const disposePointerMove = on(document, "pointermove", handlePointer, {
44
- capture: true,
45
- once: true,
46
- });
47
- callbacksToDispose.push(disposePointerDown, disposePointerMove);
48
- };
49
- const disposeKeydown = on(document, "keydown", handleKeydown, {
50
- capture: true,
51
- });
52
- callbacksToDispose.push(disposeKeydown);
53
- return () => {
54
- executeCallbacks(callbacksToDispose);
55
- };
56
- });
57
32
  }
58
33
  getAttr(name) {
59
34
  return `data-${this.variant.current}-${name}`;
@@ -140,7 +115,7 @@ class MenuContentState {
140
115
  return;
141
116
  const handler = () => {
142
117
  afterTick(() => {
143
- if (!this.parentMenu.root.isUsingKeyboard)
118
+ if (!this.parentMenu.root.isUsingKeyboard.current)
144
119
  return;
145
120
  this.rovingFocusGroup.focusFirstCandidate();
146
121
  });
@@ -211,7 +186,7 @@ class MenuContentState {
211
186
  }
212
187
  }
213
188
  onfocus(_) {
214
- if (!this.parentMenu.root.isUsingKeyboard)
189
+ if (!this.parentMenu.root.isUsingKeyboard.current)
215
190
  return;
216
191
  afterTick(() => this.rovingFocusGroup.focusFirstCandidate());
217
192
  }
@@ -386,7 +361,7 @@ class MenuItemState {
386
361
  this.#onSelect.current(selectEvent);
387
362
  afterTick(() => {
388
363
  if (selectEvent.defaultPrevented) {
389
- this.#item.content.parentMenu.root.isUsingKeyboard = false;
364
+ this.#item.content.parentMenu.root.isUsingKeyboard.current = false;
390
365
  return;
391
366
  }
392
367
  if (this.#closeOnSelect.current) {
@@ -1,7 +1,6 @@
1
1
  import { getLocalTimeZone, isSameDay, isSameMonth, isToday, } from "@internationalized/date";
2
- import { untrack } from "svelte";
3
2
  import { useRefById } from "svelte-toolbelt";
4
- import { Context } from "runed";
3
+ import { Context, watch } from "runed";
5
4
  import { CalendarRootContext } from "../calendar/calendar.svelte.js";
6
5
  import { useId } from "../../internal/use-id.js";
7
6
  import { getAriaDisabled, getAriaSelected, getDataDisabled, getDataSelected, getDataUnavailable, } from "../../internal/attrs.js";
@@ -119,65 +118,61 @@ export class RangeCalendarRootState {
119
118
  * Synchronize the start and end values with the `value` in case
120
119
  * it is updated externally.
121
120
  */
122
- $effect(() => {
123
- const value = this.value.current;
124
- untrack(() => {
125
- if (value.start && value.end) {
126
- this.startValue.current = value.start;
127
- this.endValue.current = value.end;
128
- }
129
- else if (value.start) {
130
- this.startValue.current = value.start;
131
- this.endValue.current = undefined;
132
- }
133
- });
121
+ watch(() => this.value.current, (value) => {
122
+ if (value.start && value.end) {
123
+ this.startValue.current = value.start;
124
+ this.endValue.current = value.end;
125
+ }
126
+ else if (value.start) {
127
+ this.startValue.current = value.start;
128
+ this.endValue.current = undefined;
129
+ }
130
+ else if (value.start === undefined && value.end === undefined) {
131
+ this.startValue.current = undefined;
132
+ this.endValue.current = undefined;
133
+ }
134
134
  });
135
135
  /**
136
136
  * Synchronize the placeholder value with the current start value
137
137
  */
138
- $effect(() => {
139
- this.value.current;
140
- untrack(() => {
141
- const startValue = this.value.current.start;
142
- if (startValue && this.placeholder.current !== startValue) {
143
- this.placeholder.current = startValue;
144
- }
145
- });
138
+ watch(() => this.value.current, (value) => {
139
+ const startValue = value.start;
140
+ if (startValue && this.placeholder.current !== startValue) {
141
+ this.placeholder.current = startValue;
142
+ }
146
143
  });
147
- $effect(() => {
148
- const startValue = this.startValue.current;
149
- const endValue = this.endValue.current;
150
- untrack(() => {
151
- const value = this.value.current;
152
- if (value && value.start === startValue && value.end === endValue) {
153
- return;
154
- }
155
- if (startValue && endValue) {
156
- this.#updateValue((prev) => {
157
- if (prev.start === startValue && prev.end === endValue)
158
- return prev;
159
- if (isBefore(endValue, startValue)) {
160
- const start = startValue;
161
- const end = endValue;
162
- this.#setStartValue(end);
163
- this.#setEndValue(start);
164
- return { start: endValue, end: startValue };
165
- }
166
- else {
167
- return {
168
- start: startValue,
169
- end: endValue,
170
- };
171
- }
172
- });
173
- }
174
- else if (value && value.start && value.end) {
175
- this.value.current = {
176
- start: undefined,
177
- end: undefined,
178
- };
179
- }
180
- });
144
+ watch([() => this.startValue.current, () => this.endValue.current], ([startValue, endValue]) => {
145
+ if (this.value.current &&
146
+ this.value.current.start === startValue &&
147
+ this.value.current.end === endValue) {
148
+ return;
149
+ }
150
+ if (startValue && endValue) {
151
+ this.#updateValue((prev) => {
152
+ if (prev.start === startValue && prev.end === endValue) {
153
+ return prev;
154
+ }
155
+ if (isBefore(endValue, startValue)) {
156
+ const start = startValue;
157
+ const end = endValue;
158
+ this.#setStartValue(end);
159
+ this.#setEndValue(start);
160
+ return { start: endValue, end: startValue };
161
+ }
162
+ else {
163
+ return {
164
+ start: startValue,
165
+ end: endValue,
166
+ };
167
+ }
168
+ });
169
+ }
170
+ else if (this.value.current &&
171
+ this.value.current.start &&
172
+ this.value.current.end) {
173
+ this.value.current.start = undefined;
174
+ this.value.current.end = undefined;
175
+ }
181
176
  });
182
177
  this.shiftFocus = this.shiftFocus.bind(this);
183
178
  this.handleCellClick = this.handleCellClick.bind(this);
@@ -0,0 +1,13 @@
1
+ import { type AnyFn } from "svelte-toolbelt";
2
+ /**
3
+ * Detects whether the user is currently using the keyboard
4
+ * or not by listening to keyboard and pointer events. Uses shared global
5
+ * state to avoid listener duplication.
6
+ */
7
+ export declare class IsUsingKeyboard {
8
+ static _refs: number;
9
+ static _cleanup?: AnyFn;
10
+ constructor();
11
+ get current(): boolean;
12
+ set current(value: boolean);
13
+ }
@@ -0,0 +1,51 @@
1
+ import { executeCallbacks } from "svelte-toolbelt";
2
+ import { on } from "svelte/events";
3
+ // Using global state to avoid multiple listeners.
4
+ let isUsingKeyboard = $state(false);
5
+ /**
6
+ * Detects whether the user is currently using the keyboard
7
+ * or not by listening to keyboard and pointer events. Uses shared global
8
+ * state to avoid listener duplication.
9
+ */
10
+ export class IsUsingKeyboard {
11
+ static _refs = 0; // Reference counting to avoid multiple listeners.
12
+ static _cleanup;
13
+ constructor() {
14
+ $effect(() => {
15
+ if (IsUsingKeyboard._refs === 0) {
16
+ IsUsingKeyboard._cleanup = $effect.root(() => {
17
+ const callbacksToDispose = [];
18
+ const handlePointer = (_) => {
19
+ isUsingKeyboard = false;
20
+ };
21
+ const handleKeydown = (_) => {
22
+ isUsingKeyboard = true;
23
+ };
24
+ callbacksToDispose.push(on(document, "pointerdown", handlePointer, {
25
+ capture: true,
26
+ }), on(document, "pointermove", handlePointer, {
27
+ capture: true,
28
+ }), on(document, "keydown", handleKeydown, {
29
+ capture: true,
30
+ }));
31
+ // Don't forget to spread and call twice.
32
+ return executeCallbacks(...callbacksToDispose);
33
+ });
34
+ }
35
+ IsUsingKeyboard._refs++;
36
+ return () => {
37
+ IsUsingKeyboard._refs--;
38
+ if (IsUsingKeyboard._refs === 0) {
39
+ isUsingKeyboard = false;
40
+ IsUsingKeyboard._cleanup?.();
41
+ }
42
+ };
43
+ });
44
+ }
45
+ get current() {
46
+ return isUsingKeyboard;
47
+ }
48
+ set current(value) {
49
+ isUsingKeyboard = value;
50
+ }
51
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { Accordion, AlertDialog, AspectRatio, Avatar, Button, Calendar, Checkbox, Collapsible, Combobox, Command, ContextMenu, DateField, DatePicker, DateRangeField, DateRangePicker, Dialog, DropdownMenu, Label, LinkPreview, Menubar, NavigationMenu, Pagination, PinInput, Popover, Progress, RadioGroup, RangeCalendar, ScrollArea, Select, Separator, Slider, Switch, Tabs, Toggle, ToggleGroup, Toolbar, Tooltip, Portal, } from "./bits/index.js";
1
+ export { Accordion, AlertDialog, AspectRatio, Avatar, Button, Calendar, Checkbox, Collapsible, Combobox, Command, ContextMenu, DateField, DatePicker, DateRangeField, DateRangePicker, Dialog, DropdownMenu, Label, LinkPreview, Menubar, NavigationMenu, Pagination, PinInput, Popover, Progress, RadioGroup, RangeCalendar, ScrollArea, Select, Separator, Slider, Switch, Tabs, Toggle, ToggleGroup, Toolbar, Tooltip, Portal, IsUsingKeyboard, } from "./bits/index.js";
2
2
  export * from "./shared/index.js";
3
3
  export type * from "./shared/index.js";
4
4
  export * from "./types.js";
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { Accordion, AlertDialog, AspectRatio, Avatar, Button, Calendar, Checkbox, Collapsible, Combobox, Command, ContextMenu, DateField, DatePicker, DateRangeField, DateRangePicker, Dialog, DropdownMenu, Label, LinkPreview, Menubar, NavigationMenu, Pagination, PinInput, Popover, Progress, RadioGroup, RangeCalendar, ScrollArea, Select, Separator, Slider, Switch, Tabs, Toggle, ToggleGroup, Toolbar, Tooltip, Portal, } from "./bits/index.js";
1
+ export { Accordion, AlertDialog, AspectRatio, Avatar, Button, Calendar, Checkbox, Collapsible, Combobox, Command, ContextMenu, DateField, DatePicker, DateRangeField, DateRangePicker, Dialog, DropdownMenu, Label, LinkPreview, Menubar, NavigationMenu, Pagination, PinInput, Popover, Progress, RadioGroup, RangeCalendar, ScrollArea, Select, Separator, Slider, Switch, Tabs, Toggle, ToggleGroup, Toolbar, Tooltip, Portal, IsUsingKeyboard, } from "./bits/index.js";
2
2
  export * from "./shared/index.js";
3
3
  export * from "./types.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "1.0.0-next.79",
3
+ "version": "1.0.0-next.80",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",