Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

next: Tags Input #862

Open
wants to merge 16 commits into
base: next
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/tender-tigers-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"bits-ui": patch
---

New Component: Tags Input (preview)
13 changes: 5 additions & 8 deletions packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ import {
getDataOrientation,
} from "$lib/internal/attrs.js";
import { kbd } from "$lib/internal/kbd.js";
import {
type UseRovingFocusReturn,
useRovingFocus,
} from "$lib/internal/use-roving-focus.svelte.js";
import { RovingFocusGroup } from "$lib/internal/use-roving-focus.svelte.js";
import type { Orientation } from "$lib/shared/index.js";
import { createContext } from "$lib/internal/create-context.js";

Expand Down Expand Up @@ -40,7 +37,7 @@ class AccordionBaseState {
disabled: AccordionBaseStateProps["disabled"];
#loop: AccordionBaseStateProps["loop"];
orientation: AccordionBaseStateProps["orientation"];
rovingFocusGroup: UseRovingFocusReturn;
rovingFocusGroup: RovingFocusGroup;

constructor(props: AccordionBaseStateProps) {
this.#id = props.id;
Expand All @@ -54,9 +51,9 @@ class AccordionBaseState {

this.orientation = props.orientation;
this.#loop = props.loop;
this.rovingFocusGroup = useRovingFocus({
this.rovingFocusGroup = new RovingFocusGroup({
rootNodeId: this.#id,
candidateAttr: ACCORDION_TRIGGER_ATTR,
candidateSelector: `[${ACCORDION_TRIGGER_ATTR}]:not([data-disabled])`,
loop: this.#loop,
orientation: this.orientation,
});
Expand Down Expand Up @@ -235,7 +232,7 @@ class AccordionTriggerState {
return;
}

this.#root.rovingFocusGroup.handleKeydown(this.#ref.current, e);
this.#root.rovingFocusGroup.handleKeydown({ node: this.#ref.current, event: e });
};

props = $derived.by(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
id = useId(),
ref = $bindable(null),
child,
defaultValue,
defaultValue = "",
value = $bindable(defaultValue),
...restProps
}: ComboboxInputProps = $props();

Expand All @@ -19,14 +20,14 @@
() => ref,
(v) => (ref = v)
),
value: box.with(
() => value,
(v) => (value = v)
),
});

if (defaultValue) {
inputState.root.inputValue = defaultValue;
}

const mergedProps = $derived(
mergeProps(restProps, inputState.props, { value: inputState.root.inputValue })
mergeProps(restProps, inputState.props, { value: inputState.root.inputValue.current })
);
</script>

Expand Down
10 changes: 9 additions & 1 deletion packages/bits-ui/src/lib/bits/combobox/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { HTMLInputAttributes } from "svelte/elements";
import type { BitsPrimitiveInputAttributes } from "$lib/shared/attributes.js";
import type { WithChild, Without } from "$lib/internal/types.js";

Expand Down Expand Up @@ -40,7 +41,14 @@ export type ComboboxInputPropsWithoutHTML = WithChild<{
* the input when the combobox is first mounted if there is already a value set.
*/
defaultValue?: string;

/**
* The value of the input. This should not be used to handle search queries, but rather for
* more complex controlled use cases. For search queries, use the `oninput` event handler as
* Bits UI handles the value internally.
*/
value?: string;
}>;

export type ComboboxInputProps = ComboboxInputPropsWithoutHTML &
Without<Omit<BitsPrimitiveInputAttributes, "value">, ComboboxInputPropsWithoutHTML>;
Without<BitsPrimitiveInputAttributes, ComboboxInputPropsWithoutHTML>;
1 change: 1 addition & 0 deletions packages/bits-ui/src/lib/bits/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export { Separator } from "./separator/index.js";
export { Slider } from "./slider/index.js";
export { Switch } from "./switch/index.js";
export { Tabs } from "./tabs/index.js";
export { TagsInput } from "./tags-input/index.js";
export { Toggle } from "./toggle/index.js";
export { ToggleGroup } from "./toggle-group/index.js";
export { Toolbar } from "./toolbar/index.js";
Expand Down
10 changes: 5 additions & 5 deletions packages/bits-ui/src/lib/bits/menu/menu.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { addEventListener } from "$lib/internal/events.js";
import type { AnyFn, WithRefProps } from "$lib/internal/types.js";
import { useDOMTypeahead } from "$lib/internal/use-dom-typeahead.svelte.js";
import { isElement, isElementOrSVGElement, isHTMLElement } from "$lib/internal/is.js";
import { useRovingFocus } from "$lib/internal/use-roving-focus.svelte.js";
import { RovingFocusGroup } from "$lib/internal/use-roving-focus.svelte.js";
import { kbd } from "$lib/internal/kbd.js";
import {
getAriaChecked,
Expand Down Expand Up @@ -180,7 +180,7 @@ class MenuContentState {
#pointerDir = $state<Side>("right");
#lastPointerX = $state(0);
#handleTypeaheadSearch: ReturnType<typeof useDOMTypeahead>["handleTypeaheadSearch"];
rovingFocusGroup: ReturnType<typeof useRovingFocus>;
rovingFocusGroup: RovingFocusGroup;
isMounted: MenuContentStateProps["isMounted"];
isFocusWithin = new IsFocusWithin(() => this.parentMenu.contentNode ?? undefined);

Expand Down Expand Up @@ -208,9 +208,9 @@ class MenuContentState {
});

this.#handleTypeaheadSearch = useDOMTypeahead().handleTypeaheadSearch;
this.rovingFocusGroup = useRovingFocus({
this.rovingFocusGroup = new RovingFocusGroup({
rootNodeId: this.parentMenu.contentId,
candidateAttr: this.parentMenu.root.getAttr("item"),
candidateSelector: `[${this.parentMenu.root.getAttr("item")}]:not([data-disabled])`,
loop: this.#loop,
orientation: box.with(() => "vertical"),
});
Expand Down Expand Up @@ -249,7 +249,7 @@ class MenuContentState {
const isModifierKey = e.ctrlKey || e.altKey || e.metaKey;
const isCharacterKey = e.key.length === 1;

const kbdFocusedEl = this.rovingFocusGroup.handleKeydown(target, e);
const kbdFocusedEl = this.rovingFocusGroup.handleKeydown({ node: target, event: e });
if (kbdFocusedEl) return;

// prevent space from being considered with typeahead
Expand Down
13 changes: 5 additions & 8 deletions packages/bits-ui/src/lib/bits/menubar/menubar.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ import { type ReadableBox, afterTick, box, useRefById } from "svelte-toolbelt";
import { untrack } from "svelte";
import type { InteractOutsideBehaviorType } from "../utilities/dismissible-layer/types.js";
import type { ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box.svelte.js";
import {
type UseRovingFocusReturn,
useRovingFocus,
} from "$lib/internal/use-roving-focus.svelte.js";
import { RovingFocusGroup } from "$lib/internal/use-roving-focus.svelte.js";
import type { Direction } from "$lib/shared/index.js";
import { createContext } from "$lib/internal/create-context.js";
import { getAriaExpanded, getDataDisabled, getDataOpenClosed } from "$lib/internal/attrs.js";
Expand Down Expand Up @@ -33,7 +30,7 @@ class MenubarRootState {
value: MenubarRootStateProps["value"];
dir: MenubarRootStateProps["dir"];
loop: MenubarRootStateProps["loop"];
rovingFocusGroup: UseRovingFocusReturn;
rovingFocusGroup: RovingFocusGroup;
currentTabStopId = box<string | null>(null);
wasOpenedByKeyboard = $state(false);
triggerIds = $state<string[]>([]);
Expand All @@ -50,9 +47,9 @@ class MenubarRootState {
id: this.id,
ref: this.ref,
});
this.rovingFocusGroup = useRovingFocus({
this.rovingFocusGroup = new RovingFocusGroup({
rootNodeId: this.id,
candidateAttr: TRIGGER_ATTR,
candidateSelector: `[${TRIGGER_ATTR}]:not([data-disabled])`,
loop: this.loop,
orientation: box.with(() => "horizontal"),
currentTabStopId: this.currentTabStopId,
Expand Down Expand Up @@ -228,7 +225,7 @@ class MenubarTriggerState {
e.preventDefault();
}

this.root.rovingFocusGroup.handleKeydown(this.menu.getTriggerNode(), e);
this.root.rovingFocusGroup.handleKeydown({ node: this.menu.getTriggerNode(), event: e });
};

#onfocus = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { useArrowNavigation } from "$lib/internal/use-arrow-navigation.js";
import { boxAutoReset } from "$lib/internal/box-auto-reset.svelte.js";
import type { ElementRef, WithRefProps } from "$lib/internal/types.js";
import { noop } from "$lib/internal/noop.js";
import { useRovingFocus } from "$lib/internal/use-roving-focus.svelte.js";
import { RovingFocusGroup } from "$lib/internal/use-roving-focus.svelte.js";

const [setNavigationMenuRootContext] =
createContext<NavigationMenuRootState>("NavigationMenu.Root");
Expand Down Expand Up @@ -353,7 +353,7 @@ class NavigationMenuListState {
#ref: NavigationMenuListStateProps["ref"];
indicatorTrackRef: NavigationMenuListStateProps["indicatorTrackRef"];
indicatorTrackId = box(useId());
rovingFocusGroup: ReturnType<typeof useRovingFocus>;
rovingFocusGroup: RovingFocusGroup;

constructor(
props: NavigationMenuListStateProps,
Expand All @@ -362,9 +362,8 @@ class NavigationMenuListState {
this.#id = props.id;
this.#ref = props.ref;
this.indicatorTrackRef = props.indicatorTrackRef;
this.rovingFocusGroup = useRovingFocus({
this.rovingFocusGroup = new RovingFocusGroup({
rootNodeId: this.#id,
candidateAttr: TRIGGER_ATTR,
candidateSelector: `:is([${TRIGGER_ATTR}], [data-list-link]):not([data-disabled])`,
loop: box.with(() => false),
orientation: this.menu.orientation,
Expand Down Expand Up @@ -581,7 +580,7 @@ class NavigationMenuTriggerState {
e.preventDefault();
return;
}
this.item.list.rovingFocusGroup.handleKeydown(this.#ref.current, e);
this.item.list.rovingFocusGroup.handleKeydown({ node: this.#ref.current, event: e });
};

props = $derived.by(
Expand Down Expand Up @@ -669,7 +668,7 @@ class NavigationMenuLinkState {
};

#onkeydown = (e: KeyboardEvent) => {
this.item.list.rovingFocusGroup.handleKeydown(this.#ref.current, e);
this.item.list.rovingFocusGroup.handleKeydown({ node: this.#ref.current, event: e });
};

props = $derived.by(
Expand Down
19 changes: 10 additions & 9 deletions packages/bits-ui/src/lib/bits/radio-group/radio-group.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import type { ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box
import type { WithRefProps } from "$lib/internal/types.js";
import { getAriaChecked, getAriaRequired, getDataDisabled } from "$lib/internal/attrs.js";
import type { Orientation } from "$lib/shared/index.js";
import {
type UseRovingFocusReturn,
useRovingFocus,
} from "$lib/internal/use-roving-focus.svelte.js";
import { RovingFocusGroup } from "$lib/internal/use-roving-focus.svelte.js";
import { createContext } from "$lib/internal/create-context.js";
import { kbd } from "$lib/internal/kbd.js";

Expand All @@ -33,7 +30,7 @@ class RadioGroupRootState {
orientation: RadioGroupRootStateProps["orientation"];
name: RadioGroupRootStateProps["name"];
value: RadioGroupRootStateProps["value"];
rovingFocusGroup: UseRovingFocusReturn;
rovingFocusGroup: RovingFocusGroup;
hasValue = $derived.by(() => this.value.current !== "");

constructor(props: RadioGroupRootStateProps) {
Expand All @@ -45,10 +42,9 @@ class RadioGroupRootState {
this.name = props.name;
this.value = props.value;
this.#ref = props.ref;

this.rovingFocusGroup = useRovingFocus({
this.rovingFocusGroup = new RovingFocusGroup({
rootNodeId: this.#id,
candidateAttr: RADIO_GROUP_ITEM_ATTR,
candidateSelector: `[${RADIO_GROUP_ITEM_ATTR}]:not([data-disabled])`,
loop: this.loop,
orientation: this.orientation,
});
Expand Down Expand Up @@ -135,7 +131,12 @@ class RadioGroupItemState {
this.#root.setValue(this.#value.current);
return;
}
this.#root.rovingFocusGroup.handleKeydown(this.#ref.current, e, true);
this.#root.rovingFocusGroup.handleKeydown({
node: this.#ref.current,
event: e,
orientation: this.#root.orientation.current,
both: true,
});
};

#tabIndex = $state(0);
Expand Down
18 changes: 10 additions & 8 deletions packages/bits-ui/src/lib/bits/select/select.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Previous } from "runed";
import { untrack } from "svelte";
import { afterTick, srOnlyStyles, styleToString, useRefById } from "svelte-toolbelt";
import { afterTick, box, srOnlyStyles, styleToString, useRefById } from "svelte-toolbelt";
import { backward, forward, next, prev } from "$lib/internal/arrays.js";
import {
getAriaExpanded,
Expand Down Expand Up @@ -54,7 +54,7 @@ class SelectBaseRootState {
items: SelectBaseRootStateProps["items"];
allowDeselect: SelectBaseRootStateProps["allowDeselect"];
touchedInput = $state(false);
inputValue = $state<string>("");
inputValue: SelectInputStateProps["value"] = box("");
inputNode = $state<HTMLElement | null>(null);
contentNode = $state<HTMLElement | null>(null);
triggerNode = $state<HTMLElement | null>(null);
Expand Down Expand Up @@ -198,7 +198,7 @@ class SelectSingleRootState extends SelectBaseRootState {

toggleItem = (itemValue: string, itemLabel: string = itemValue) => {
this.value.current = this.includesItem(itemValue) ? "" : itemValue;
this.inputValue = itemLabel;
this.inputValue.current = itemLabel;
};

setInitialHighlightedNode = () => {
Expand Down Expand Up @@ -242,7 +242,8 @@ class SelectMultipleRootState extends SelectBaseRootState {
}

includesItem = (itemValue: string) => {
return this.value.current.includes(itemValue);
const ss = $state.snapshot(this.value.current);
return ss.includes(itemValue);
};

toggleItem = (itemValue: string, itemLabel: string = itemValue) => {
Expand All @@ -251,7 +252,7 @@ class SelectMultipleRootState extends SelectBaseRootState {
} else {
this.value.current = [...this.value.current, itemValue];
}
this.inputValue = itemLabel;
this.inputValue.current = itemLabel;
};

setInitialHighlightedNode = () => {
Expand All @@ -272,7 +273,7 @@ class SelectMultipleRootState extends SelectBaseRootState {

type SelectRootState = SelectSingleRootState | SelectMultipleRootState;

type SelectInputStateProps = WithRefProps;
type SelectInputStateProps = WithRefProps & WritableBoxedValues<{ value: string }>;

class SelectInputState {
#id: SelectInputStateProps["id"];
Expand All @@ -283,6 +284,7 @@ class SelectInputState {
this.root = root;
this.#id = props.id;
this.#ref = props.ref;
this.root.inputValue = props.value;

useRefById({
id: this.#id,
Expand All @@ -297,7 +299,7 @@ class SelectInputState {
this.root.isUsingKeyboard = true;
if (e.key === kbd.ESCAPE) return;
const open = this.root.open.current;
const inputValue = this.root.inputValue;
const inputValue = this.root.inputValue.current;

// prevent arrow up/down from moving the position of the cursor in the input
if (e.key === kbd.ARROW_UP || e.key === kbd.ARROW_DOWN) e.preventDefault();
Expand Down Expand Up @@ -389,7 +391,7 @@ class SelectInputState {
};

#oninput = (e: Event & { currentTarget: HTMLInputElement }) => {
this.root.inputValue = e.currentTarget.value;
this.root.inputValue.current = e.currentTarget.value;
afterTick(() => {
this.root.setHighlightedToFirstCandidate();
});
Expand Down
Loading
Loading