bits-ui 2.17.2 → 2.18.0

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.
@@ -25,6 +25,5 @@
25
25
  _internal_variant="menubar"
26
26
  {...restProps}
27
27
  _internal_should_skip_exit_animation={() =>
28
- menuState.root.skipExitAnimationForMenuValue === menuState.opts.value.current
29
- }
28
+ menuState.root.skipExitAnimationForMenuValue === menuState.opts.value.current}
30
29
  />
@@ -11,6 +11,7 @@
11
11
  id = createId(uid),
12
12
  inputId = `${createId(uid)}-input`,
13
13
  ref = $bindable(null),
14
+ inputRef = $bindable(null),
14
15
  maxlength = 6,
15
16
  textalign = "left",
16
17
  pattern,
@@ -33,6 +34,10 @@
33
34
  () => ref,
34
35
  (v) => (ref = v)
35
36
  ),
37
+ inputRef: boxWith(
38
+ () => inputRef,
39
+ (v) => (inputRef = v)
40
+ ),
36
41
  inputId: boxWith(() => inputId),
37
42
  autocomplete: boxWith(() => autocomplete),
38
43
  maxLength: boxWith(() => maxlength),
@@ -1,4 +1,4 @@
1
1
  import type { PinInputRootProps } from "../types.js";
2
- declare const PinInput: import("svelte").Component<PinInputRootProps, {}, "value" | "ref">;
2
+ declare const PinInput: import("svelte").Component<PinInputRootProps, {}, "value" | "inputRef" | "ref">;
3
3
  type PinInput = ReturnType<typeof PinInput>;
4
4
  export default PinInput;
@@ -6,6 +6,7 @@ export declare const REGEXP_ONLY_CHARS = "^[a-zA-Z]+$";
6
6
  export declare const REGEXP_ONLY_DIGITS_AND_CHARS = "^[a-zA-Z0-9]+$";
7
7
  interface PinInputRootStateOpts extends WithRefOpts, WritableBoxedValues<{
8
8
  value: string;
9
+ inputRef: HTMLInputElement | null;
9
10
  }>, ReadableBoxedValues<{
10
11
  inputId: string;
11
12
  disabled: boolean;
@@ -33,9 +33,8 @@ export class PinInputRootState {
33
33
  }
34
34
  opts;
35
35
  attachment;
36
- #inputRef = simpleBox(null);
36
+ inputAttachment;
37
37
  #isHoveringInput = $state(false);
38
- inputAttachment = attachRef(this.#inputRef);
39
38
  #isFocused = simpleBox(false);
40
39
  #mirrorSelectionStart = $state(null);
41
40
  #mirrorSelectionEnd = $state(null);
@@ -58,6 +57,7 @@ export class PinInputRootState {
58
57
  constructor(opts) {
59
58
  this.opts = opts;
60
59
  this.attachment = attachRef(this.opts.ref);
60
+ this.inputAttachment = attachRef(this.opts.inputRef);
61
61
  this.domContext = new DOMContext(opts.ref);
62
62
  this.#initialLoad = {
63
63
  value: this.opts.value,
@@ -66,13 +66,13 @@ export class PinInputRootState {
66
66
  };
67
67
  this.#pwmb = usePasswordManagerBadge({
68
68
  containerRef: this.opts.ref,
69
- inputRef: this.#inputRef,
69
+ inputRef: this.opts.inputRef,
70
70
  isFocused: this.#isFocused,
71
71
  pushPasswordManagerStrategy: this.opts.pushPasswordManagerStrategy,
72
72
  domContext: this.domContext,
73
73
  });
74
74
  onMount(() => {
75
- const input = this.#inputRef.current;
75
+ const input = this.opts.inputRef.current;
76
76
  const container = this.opts.ref.current;
77
77
  if (!input || !container)
78
78
  return;
@@ -107,9 +107,9 @@ export class PinInputRootState {
107
107
  resizeObserver.disconnect();
108
108
  };
109
109
  });
110
- watch([() => this.opts.value.current, () => this.#inputRef.current], () => {
110
+ watch([() => this.opts.value.current, () => this.opts.inputRef.current], () => {
111
111
  syncTimeouts(() => {
112
- const input = this.#inputRef.current;
112
+ const input = this.opts.inputRef.current;
113
113
  if (!input)
114
114
  return;
115
115
  // forcefully remove :autofill state
@@ -213,7 +213,7 @@ export class PinInputRootState {
213
213
  }
214
214
  }
215
215
  #onDocumentSelectionChange = () => {
216
- const input = this.#inputRef.current;
216
+ const input = this.opts.inputRef.current;
217
217
  const container = this.opts.ref.current;
218
218
  if (!input || !container)
219
219
  return;
@@ -260,7 +260,7 @@ export class PinInputRootState {
260
260
  }
261
261
  }
262
262
  if (start !== -1 && end !== -1 && start !== end) {
263
- this.#inputRef.current?.setSelectionRange(start, end, direction);
263
+ this.opts.inputRef.current?.setSelectionRange(start, end, direction);
264
264
  }
265
265
  }
266
266
  // finally update the state
@@ -289,7 +289,7 @@ export class PinInputRootState {
289
289
  this.opts.value.current = newValue;
290
290
  };
291
291
  onfocus = (_) => {
292
- const input = this.#inputRef.current;
292
+ const input = this.opts.inputRef.current;
293
293
  if (input) {
294
294
  const start = Math.min(input.value.length, this.opts.maxLength.current - 1);
295
295
  const end = input.value.length;
@@ -300,7 +300,7 @@ export class PinInputRootState {
300
300
  this.#isFocused.current = true;
301
301
  };
302
302
  onpaste = (e) => {
303
- const input = this.#inputRef.current;
303
+ const input = this.opts.inputRef.current;
304
304
  if (!input)
305
305
  return;
306
306
  const getNewValue = (finalContent) => {
@@ -52,6 +52,12 @@ export type PinInputRootPropsWithoutHTML = Omit<WithChild<{
52
52
  * Optionally provide an ID to apply to the hidden input element.
53
53
  */
54
54
  inputId?: string;
55
+ /**
56
+ * The underlying hidden `<input>` element. Bind to call `focus()`, read selection, etc.
57
+ *
58
+ * @bindable
59
+ */
60
+ inputRef?: HTMLInputElement | null;
55
61
  /**
56
62
  * The children snippet used to render the individual cells.
57
63
  */
@@ -0,0 +1,48 @@
1
+ <script lang="ts">
2
+ import { boxWith, mergeProps } from "svelte-toolbelt";
3
+ import { SelectValueState } from "../select.svelte.js";
4
+ import type { SelectValueProps } from "../types.js";
5
+ import { createId } from "../../../internal/create-id.js";
6
+
7
+ const uid = $props.id();
8
+
9
+ let {
10
+ ref = $bindable(null),
11
+ id = createId(uid),
12
+ placeholder,
13
+ child,
14
+ children,
15
+ ...restProps
16
+ }: SelectValueProps = $props();
17
+
18
+ const valueState = SelectValueState.create({
19
+ id: boxWith(() => id),
20
+ ref: boxWith(
21
+ () => ref,
22
+ (v) => (ref = v)
23
+ ),
24
+ placeholder: boxWith(() => placeholder),
25
+ });
26
+
27
+ const mergedProps = $derived(mergeProps(restProps, valueState.props));
28
+ </script>
29
+
30
+ {#if child}
31
+ {@render child({ props: mergedProps, ...valueState.snippetProps })}
32
+ {:else}
33
+ <span {...mergedProps}>
34
+ {#if children}
35
+ {@render children?.(valueState.snippetProps)}
36
+ {:else if valueState.snippetProps.selection.type === "single"}
37
+ {valueState.snippetProps.selection.selected?.label ?? placeholder}
38
+ {:else if valueState.snippetProps.selection.type === "multiple" && valueState.snippetProps.selection.selected}
39
+ {valueState.snippetProps.selection.selected.length > 0
40
+ ? valueState.snippetProps.selection.selected
41
+ .map((selected) => selected.label)
42
+ .join(", ")
43
+ : placeholder}
44
+ {:else}
45
+ {placeholder}
46
+ {/if}
47
+ </span>
48
+ {/if}
@@ -0,0 +1,4 @@
1
+ import type { SelectValueProps } from "../types.js";
2
+ declare const SelectValue: import("svelte").Component<SelectValueProps, {}, "ref">;
3
+ type SelectValue = ReturnType<typeof SelectValue>;
4
+ export default SelectValue;
@@ -1,4 +1,5 @@
1
1
  export { default as Root } from "./components/select.svelte";
2
+ export { default as Value } from "./components/select-value.svelte";
2
3
  export { default as Content } from "./components/select-content.svelte";
3
4
  export { default as ContentStatic } from "./components/select-content-static.svelte";
4
5
  export { default as Item } from "./components/select-item.svelte";
@@ -9,4 +10,4 @@ export { default as Portal } from "../utilities/portal/portal.svelte";
9
10
  export { default as Viewport } from "./components/select-viewport.svelte";
10
11
  export { default as ScrollUpButton } from "./components/select-scroll-up-button.svelte";
11
12
  export { default as ScrollDownButton } from "./components/select-scroll-down-button.svelte";
12
- export type { SelectRootProps as RootProps, SelectContentProps as ContentProps, SelectContentStaticProps as ContentStaticProps, SelectItemProps as ItemProps, SelectGroupProps as GroupProps, SelectGroupHeadingProps as GroupHeadingProps, SelectTriggerProps as TriggerProps, SelectViewportProps as ViewportProps, SelectScrollUpButtonProps as ScrollUpButtonProps, SelectScrollDownButtonProps as ScrollDownButtonProps, SelectPortalProps as PortalProps, } from "./types.js";
13
+ export type { SelectRootProps as RootProps, SelectValueProps as ValueProps, SelectContentProps as ContentProps, SelectContentStaticProps as ContentStaticProps, SelectItemProps as ItemProps, SelectGroupProps as GroupProps, SelectGroupHeadingProps as GroupHeadingProps, SelectTriggerProps as TriggerProps, SelectViewportProps as ViewportProps, SelectScrollUpButtonProps as ScrollUpButtonProps, SelectScrollDownButtonProps as ScrollDownButtonProps, SelectPortalProps as PortalProps, } from "./types.js";
@@ -1,4 +1,5 @@
1
1
  export { default as Root } from "./components/select.svelte";
2
+ export { default as Value } from "./components/select-value.svelte";
2
3
  export { default as Content } from "./components/select-content.svelte";
3
4
  export { default as ContentStatic } from "./components/select-content-static.svelte";
4
5
  export { default as Item } from "./components/select-item.svelte";
@@ -2,6 +2,7 @@ import { Previous } from "runed";
2
2
  import { DOMContext, type ReadableBoxedValues, type WritableBoxedValues, type Box } from "svelte-toolbelt";
3
3
  import type { BitsEvent, BitsFocusEvent, BitsKeyboardEvent, BitsMouseEvent, BitsPointerEvent, OnChangeFn, WithRefOpts, RefAttachment } from "../../internal/types.js";
4
4
  import { PresenceManager } from "../../internal/presence-manager.svelte.js";
5
+ import type { SelectValueSnippetProps } from "./types.js";
5
6
  export declare const INTERACTION_KEYS: string[];
6
7
  export declare const FIRST_KEYS: string[];
7
8
  export declare const LAST_KEYS: string[];
@@ -36,6 +37,7 @@ declare abstract class SelectBaseRootState {
36
37
  contentPresence: PresenceManager;
37
38
  viewportNode: HTMLElement | null;
38
39
  triggerNode: HTMLElement | null;
40
+ valueNode: HTMLElement | null;
39
41
  valueId: string;
40
42
  highlightedNode: HTMLElement | null;
41
43
  readonly highlightedValue: string | null;
@@ -51,6 +53,11 @@ declare abstract class SelectBaseRootState {
51
53
  getCandidateNodes(): HTMLElement[];
52
54
  setHighlightedToFirstCandidate(initial?: boolean): void;
53
55
  getNodeByValue(value: string): HTMLElement | null;
56
+ /**
57
+ * Resolves the display label for a value: `items` entry when present, otherwise the
58
+ * mounted item's `data-label` or its text content.
59
+ */
60
+ getLabelForValue(value: string): string;
54
61
  setOpen(open: boolean): void;
55
62
  toggleOpen(): void;
56
63
  handleOpen(): void;
@@ -112,6 +119,23 @@ export declare class SelectRootState {
112
119
  static create(props: SelectRootStateOpts): SelectRoot;
113
120
  }
114
121
  type SelectRoot = SelectSingleRootState | SelectMultipleRootState;
122
+ type SelectValueStateProps = WithRefOpts<ReadableBoxedValues<{
123
+ placeholder: string | null | undefined;
124
+ }>>;
125
+ export declare class SelectValueState {
126
+ static create(opts: SelectValueStateProps): SelectValueState;
127
+ readonly root: SelectRoot;
128
+ readonly opts: SelectValueStateProps;
129
+ readonly attachment: RefAttachment;
130
+ constructor(opts: SelectValueStateProps, root: SelectRoot);
131
+ setValue(value: string | string[]): void;
132
+ readonly snippetProps: SelectValueSnippetProps;
133
+ readonly props: {
134
+ id: string;
135
+ "data-placeholder": string | undefined;
136
+ "data-select-value": string;
137
+ };
138
+ }
115
139
  interface SelectInputStateOpts extends WithRefOpts, ReadableBoxedValues<{
116
140
  clearOnDeselect: boolean;
117
141
  }> {
@@ -11,6 +11,7 @@ import { getFloatingContentCSSVars } from "../../internal/floating-svelte/floati
11
11
  import { DataTypeahead } from "../../internal/data-typeahead.svelte.js";
12
12
  import { DOMTypeahead } from "../../internal/dom-typeahead.svelte.js";
13
13
  import { PresenceManager } from "../../internal/presence-manager.svelte.js";
14
+ import { DEV } from "esm-env";
14
15
  // prettier-ignore
15
16
  export const INTERACTION_KEYS = [kbd.ARROW_LEFT, kbd.ESCAPE, kbd.ARROW_RIGHT, kbd.SHIFT, kbd.CAPS_LOCK, kbd.CONTROL, kbd.ALT, kbd.META, kbd.ENTER, kbd.F1, kbd.F2, kbd.F3, kbd.F4, kbd.F5, kbd.F6, kbd.F7, kbd.F8, kbd.F9, kbd.F10, kbd.F11, kbd.F12];
16
17
  export const FIRST_KEYS = [kbd.ARROW_DOWN, kbd.PAGE_UP, kbd.HOME];
@@ -48,6 +49,7 @@ class SelectBaseRootState {
48
49
  contentPresence;
49
50
  viewportNode = $state(null);
50
51
  triggerNode = $state(null);
52
+ valueNode = $state(null);
51
53
  valueId = $state("");
52
54
  highlightedNode = $state(null);
53
55
  highlightedValue = $derived.by(() => {
@@ -127,6 +129,25 @@ class SelectBaseRootState {
127
129
  const candidateNodes = this.getCandidateNodes();
128
130
  return candidateNodes.find((node) => node.dataset.value === value) ?? null;
129
131
  }
132
+ /**
133
+ * Resolves the display label for a value: `items` entry when present, otherwise the
134
+ * mounted item's `data-label` or its text content.
135
+ */
136
+ getLabelForValue(value) {
137
+ if (value === "")
138
+ return "";
139
+ const fromItems = this.opts.items.current.find((item) => item.value === value)?.label;
140
+ if (fromItems !== undefined)
141
+ return fromItems;
142
+ const node = this.getNodeByValue(value);
143
+ if (node) {
144
+ const dataLabel = node.getAttribute("data-label");
145
+ if (dataLabel !== null && dataLabel !== "")
146
+ return dataLabel;
147
+ return node.textContent?.trim() ?? value;
148
+ }
149
+ return value;
150
+ }
130
151
  setOpen(open) {
131
152
  this.opts.open.current = open;
132
153
  }
@@ -269,6 +290,70 @@ export class SelectRootState {
269
290
  return SelectRootContext.set(rootState);
270
291
  }
271
292
  }
293
+ export class SelectValueState {
294
+ static create(opts) {
295
+ return new SelectValueState(opts, SelectRootContext.get());
296
+ }
297
+ root;
298
+ opts;
299
+ attachment;
300
+ constructor(opts, root) {
301
+ this.root = root;
302
+ this.opts = opts;
303
+ this.attachment = attachRef(opts.ref, (v) => (this.root.valueNode = v));
304
+ this.setValue = this.setValue.bind(this);
305
+ }
306
+ setValue(value) {
307
+ if (this.root.isMulti && !Array.isArray(value)) {
308
+ if (DEV)
309
+ throw new Error(`Expected an array of strings passed to \`setValue\` got ${typeof value}.`);
310
+ return;
311
+ }
312
+ if (!this.root.isMulti && typeof value !== "string") {
313
+ if (DEV)
314
+ throw new Error(`Expected a string passed to \`setValue\` got ${typeof value}.`);
315
+ return;
316
+ }
317
+ this.root.opts.value.current = value;
318
+ }
319
+ // this way consumers get type narrowing for the value on `type`
320
+ snippetProps = $derived.by(() => {
321
+ if (this.root.isMulti) {
322
+ return {
323
+ selection: {
324
+ type: "multiple",
325
+ selected: this.root.opts.value.current.length > 0
326
+ ? this.root.opts.value.current.map((value) => ({
327
+ value,
328
+ label: this.root.getLabelForValue(value),
329
+ }))
330
+ : [],
331
+ setValue: this.setValue,
332
+ },
333
+ placeholder: this.opts.placeholder.current ?? null,
334
+ disabled: this.root.opts.disabled.current,
335
+ };
336
+ }
337
+ const value = this.root.opts.value.current;
338
+ return {
339
+ selection: {
340
+ type: "single",
341
+ selected: value !== ""
342
+ ? { value, label: value === "" ? "" : this.root.getLabelForValue(value) }
343
+ : undefined,
344
+ setValue: this.setValue,
345
+ },
346
+ placeholder: this.opts.placeholder.current ?? null,
347
+ disabled: this.root.opts.disabled.current,
348
+ };
349
+ });
350
+ props = $derived.by(() => ({
351
+ id: this.opts.id.current,
352
+ "data-placeholder": this.root.hasValue ? undefined : "",
353
+ "data-select-value": "",
354
+ ...this.attachment,
355
+ }));
356
+ }
272
357
  export class SelectInputState {
273
358
  static create(opts) {
274
359
  return new SelectInputState(opts, SelectRootContext.get());
@@ -2,7 +2,7 @@ import type { Expand } from "svelte-toolbelt";
2
2
  import type { PortalProps } from "../utilities/portal/types.js";
3
3
  import type { PopperLayerProps, PopperLayerStaticProps } from "../utilities/popper-layer/types.js";
4
4
  import type { ArrowProps, ArrowPropsWithoutHTML } from "../utilities/arrow/types.js";
5
- import type { BitsPrimitiveButtonAttributes, BitsPrimitiveDivAttributes } from "../../shared/attributes.js";
5
+ import type { BitsPrimitiveButtonAttributes, BitsPrimitiveDivAttributes, BitsPrimitiveSpanAttributes } from "../../shared/attributes.js";
6
6
  import type { OnChangeFn, WithChild, WithChildNoChildrenSnippetProps, WithChildren, Without } from "../../internal/types.js";
7
7
  import type { FloatingContentSnippetProps, StaticContentSnippetProps } from "../../shared/types.js";
8
8
  import type { HTMLInputAttributes } from "svelte/elements";
@@ -124,6 +124,27 @@ export type SelectSingleRootProps = SelectBaseRootPropsWithoutHTML & SelectSingl
124
124
  export type SelectMultipleRootProps = SelectBaseRootPropsWithoutHTML & SelectMultipleRootPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, SelectMultipleRootPropsWithoutHTML | SelectBaseRootPropsWithoutHTML>;
125
125
  export type SelectRootPropsWithoutHTML = SelectBaseRootPropsWithoutHTML & (SelectSingleRootPropsWithoutHTML | SelectMultipleRootPropsWithoutHTML);
126
126
  export type SelectRootProps = SelectRootPropsWithoutHTML;
127
+ export type SelectValueSnippetProps = {
128
+ selection: {
129
+ type: "single";
130
+ selected?: {
131
+ value: string;
132
+ label: string;
133
+ };
134
+ setValue: (value: string) => void;
135
+ } | {
136
+ type: "multiple";
137
+ selected: {
138
+ value: string;
139
+ label: string;
140
+ }[];
141
+ setValue: (value: string[]) => void;
142
+ };
143
+ placeholder: string | null;
144
+ disabled: boolean;
145
+ };
146
+ export type SelectValuePropsWithoutHTML = WithChild<{}, SelectValueSnippetProps>;
147
+ export type SelectValueProps = SelectValuePropsWithoutHTML & Without<BitsPrimitiveSpanAttributes, SelectValuePropsWithoutHTML>;
127
148
  export type _SharedSelectContentProps = {
128
149
  /**
129
150
  * Whether or not to loop through the items when navigating with the keyboard.
@@ -194,8 +194,9 @@ function isValidEvent(e, node) {
194
194
  const nodeIsContextMenu = Boolean(node.closest(`[${CONTEXT_MENU_CONTENT_ATTR}]`));
195
195
  if ("button" in e && e.button > 0 && !targetIsContextMenuTrigger)
196
196
  return false;
197
- if ("button" in e && e.button === 0 && targetIsContextMenuTrigger)
198
- return nodeIsContextMenu;
197
+ if ("button" in e && e.button === 0 && targetIsContextMenuTrigger && nodeIsContextMenu) {
198
+ return true;
199
+ }
199
200
  if (targetIsContextMenuTrigger && nodeIsContextMenu)
200
201
  return false;
201
202
  const ownerDocument = getOwnerDocument(target);
@@ -54,9 +54,7 @@
54
54
  } = $props();
55
55
 
56
56
  const resolvedPreventScroll = $derived(preventScroll ?? true);
57
- const effectiveStrategy = $derived(
58
- strategy ?? (resolvedPreventScroll ? "fixed" : "absolute")
59
- );
57
+ const effectiveStrategy = $derived(strategy ?? (resolvedPreventScroll ? "fixed" : "absolute"));
60
58
  </script>
61
59
 
62
60
  <PopperContent
@@ -12,9 +12,8 @@
12
12
  </script>
13
13
 
14
14
  {#if forceMount || open || presenceState.isPresent}
15
- {@render
16
- presence?.({
17
- present: presenceState.isPresent,
18
- transitionStatus: presenceState.transitionStatus,
19
- })}
15
+ {@render presence?.({
16
+ present: presenceState.isPresent,
17
+ transitionStatus: presenceState.transitionStatus,
18
+ })}
20
19
  {/if}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "2.17.2",
3
+ "version": "2.18.0",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",