Skip to content

Commit

Permalink
meh
Browse files Browse the repository at this point in the history
  • Loading branch information
huntabyte committed Nov 20, 2024
1 parent 7a48a54 commit 66e4efe
Show file tree
Hide file tree
Showing 13 changed files with 284 additions and 145 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script lang="ts">
import { box, mergeProps } from "svelte-toolbelt";
import type { TagsInputTagEditInputProps } from "../types.js";
import { useTagsInputTagEditInput } from "../tags-input.svelte.js";
import { useId } from "$lib/internal/use-id.js";
let {
id = useId(),
ref = $bindable(null),
child,
...restProps
}: TagsInputTagEditInputProps = $props();
const tagEditState = useTagsInputTagEditInput({
id: box.with(() => id),
ref: box.with(
() => ref,
(v) => (ref = v)
),
});
const mergedProps = $derived(mergeProps(restProps, tagEditState.props));
</script>

{#if child}
{@render child({ props: mergedProps })}
{:else}
<input {...mergedProps} />
{/if}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
id = useId(),
ref = $bindable(null),
child,
children,
disabled = false,
...restProps
}: TagsInputTagEditProps = $props();
Expand All @@ -17,6 +19,7 @@
() => ref,
(v) => (ref = v)
),
disabled: box.with(() => disabled),
});
const mergedProps = $derived(mergeProps(restProps, tagEditState.props));
Expand All @@ -25,5 +28,7 @@
{#if child}
{@render child({ props: mergedProps })}
{:else}
<input {...mergedProps} />
<button {...mergedProps}>
{@render children?.()}
</button>
{/if}
2 changes: 2 additions & 0 deletions packages/bits-ui/src/lib/bits/tags-input/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { default as Clear } from "./components/tags-input-clear.svelte";
export { default as Tag } from "./components/tags-input-tag.svelte";
export { default as TagText } from "./components/tags-input-tag-text.svelte";
export { default as TagRemove } from "./components/tags-input-tag-remove.svelte";
export { default as TagEditInput } from "./components/tags-input-tag-edit-input.svelte";
export { default as TagEdit } from "./components/tags-input-tag-edit.svelte";
export { default as TagContent } from "./components/tags-input-tag-content.svelte";

Expand All @@ -16,6 +17,7 @@ export type {
TagsInputTagProps as TagProps,
TagsInputTagTextProps as TagTextProps,
TagsInputTagRemoveProps as TagRemoveProps,
TagsInputTagEditInputProps as TagEditInputProps,
TagsInputTagEditProps as TagEditProps,
TagsInputTagContentProps as TagContentProps,
} from "./types.js";
112 changes: 96 additions & 16 deletions packages/bits-ui/src/lib/bits/tags-input/tags-input.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ import {
srOnlyStyles,
useRefById,
} from "svelte-toolbelt";
import type { FocusEventHandler, KeyboardEventHandler } from "svelte/elements";
import type {
ClipboardEventHandler,
FocusEventHandler,
HTMLButtonAttributes,
KeyboardEventHandler,
MouseEventHandler,
} from "svelte/elements";
import { IsFocusWithin } from "runed";
import type { TagsInputBlurBehavior, TagsInputPasteBehavior } from "./types.js";
import type { WithRefProps } from "$lib/internal/types.js";
import { createContext } from "$lib/internal/create-context.js";
Expand All @@ -24,6 +31,7 @@ const TAG_ATTR = "data-tags-input-tag";
const TAG_TEXT_ATTR = "data-tags-input-tag-text";
const TAG_CONTENT_ATTR = "data-tags-input-tag-content";
const TAG_REMOVE_ATTR = "data-tags-input-tag-remove";
const TAG_EDIT_INPUT_ATTR = "data-tags-input-tag-edit-input";
const TAG_EDIT_ATTR = "data-tags-input-tag-edit";

type TagsInputRootStateProps = WithRefProps &
Expand Down Expand Up @@ -240,6 +248,8 @@ class TagsInputTagState {
isEditable = $derived.by(() => this.editMode.current !== "none");
isEditing = $state(false);
#tabIndex = $state(0);
#focusWithin: IsFocusWithin;
isFocusWithin = $derived.by(() => this.#focusWithin.current);

constructor(props: TagsInputTagStateProps, list: TagsInputListState) {
this.#ref = props.ref;
Expand All @@ -257,6 +267,8 @@ class TagsInputTagState {
deps: () => this.index.current,
});

this.#focusWithin = new IsFocusWithin(() => this.#ref.current ?? undefined);

$effect(() => {
// we want to track the value here so when we remove the actively focused
// tag, we ensure the other ones get the correct tab index
Expand Down Expand Up @@ -296,7 +308,7 @@ class TagsInputTagState {
this.root.recomputeTabIndex();
};

#onkeydown = (e: KeyboardEvent) => {
#onkeydown: KeyboardEventHandler<HTMLElement> = (e) => {
if (e.target !== this.#ref.current) return;
if (HORIZONTAL_NAV_KEYS.includes(e.key)) {
e.preventDefault();
Expand Down Expand Up @@ -330,7 +342,6 @@ class TagsInputTagState {
"data-invalid": getDataInvalid(this.root.isInvalid),
tabindex: this.#tabIndex,
[TAG_ATTR]: "",
"aria-label": `${this.value.current}`,
onkeydown: this.#onkeydown,
}) as const
);
Expand Down Expand Up @@ -421,14 +432,14 @@ class TagsInputTagTextState {
);
}

type TagsInputTagEditStateProps = WithRefProps;
type TagsInputTagEditInputStateProps = WithRefProps;

class TagsInputTagEditState {
#ref: TagsInputTagEditStateProps["ref"];
#id: TagsInputTagEditStateProps["id"];
class TagsInputTagEditInputState {
#ref: TagsInputTagEditInputStateProps["ref"];
#id: TagsInputTagEditInputStateProps["id"];
tag: TagsInputTagState;

constructor(props: TagsInputTagEditStateProps, tag: TagsInputTagState) {
constructor(props: TagsInputTagEditInputStateProps, tag: TagsInputTagState) {
this.#ref = props.ref;
this.#id = props.id;
this.tag = tag;
Expand Down Expand Up @@ -477,7 +488,7 @@ class TagsInputTagEditState {
() =>
({
id: this.#id.current,
[TAG_EDIT_ATTR]: "",
[TAG_EDIT_INPUT_ATTR]: "",
tabindex: -1,
"data-editing": this.tag.isEditing ? "" : undefined,
"data-invalid": getDataInvalid(this.tag.root.isInvalid),
Expand Down Expand Up @@ -523,11 +534,11 @@ class TagsInputTagRemoveState {
});
}

#onclick = () => {
#onclick: MouseEventHandler<HTMLButtonElement> = () => {
this.#tag.remove();
};

#onkeydown = (e: KeyboardEvent) => {
#onkeydown: KeyboardEventHandler<HTMLButtonElement> = (e) => {
if (e.key === kbd.ENTER || e.key === kbd.SPACE) {
e.preventDefault();
this.#tag.remove();
Expand All @@ -551,7 +562,72 @@ class TagsInputTagRemoveState {
"data-editing": this.#tag.isEditing ? "" : undefined,
"data-editable": this.#tag.isEditable ? "" : undefined,
"data-removable": this.#tag.removable.current ? "" : undefined,
tabindex: -1,
tabindex: this.#tag.isFocusWithin ? 0 : -1,
onclick: this.#onclick,
onkeydown: this.#onkeydown,
}) as const
);
}

type TagsInputTagEditStateProps = WithRefProps &
ReadableBoxedValues<{
disabled: HTMLButtonAttributes["disabled"];
}>;

class TagsInputTagEditState {
#ref: TagsInputTagEditStateProps["ref"];
#id: TagsInputTagEditStateProps["id"];
#tag: TagsInputTagState;
#disabled: TagsInputTagEditStateProps["disabled"];
root: TagsInputRootState;
#ariaLabelledBy = $derived.by(() => {
if (this.#tag.textNode && this.#tag.textNode.id) {
return `${this.#id.current} ${this.#tag.textNode.id}`;
}
return this.#id.current;
});

constructor(props: TagsInputTagEditStateProps, tag: TagsInputTagState) {
this.#ref = props.ref;
this.#id = props.id;
this.#tag = tag;
this.root = tag.root;
this.#disabled = props.disabled;

useRefById({
id: this.#id,
ref: this.#ref,
onRefChange: (node) => {
this.#tag.removeNode = node;
},
});
}

#onclick: MouseEventHandler<HTMLButtonElement> = () => {
if (this.#disabled.current) return;
this.#tag.startEditing();
};

#onkeydown: KeyboardEventHandler<HTMLButtonElement> = (e) => {
if (this.#disabled.current) return;
if (e.key === kbd.ENTER || e.key === kbd.SPACE) {
e.preventDefault();
this.#tag.startEditing();
}
};

props = $derived.by(
() =>
({
id: this.#id.current,
[TAG_EDIT_ATTR]: "",
role: "button",
"aria-label": "Remove",
"aria-labelledby": this.#ariaLabelledBy,
"data-editing": this.#tag.isEditing ? "" : undefined,
"data-editable": this.#tag.isEditable ? "" : undefined,
"data-removable": this.#tag.removable.current ? "" : undefined,
tabindex: this.#tag.isFocusWithin ? 0 : -1,
onclick: this.#onclick,
onkeydown: this.#onkeydown,
}) as const
Expand Down Expand Up @@ -594,7 +670,7 @@ class TagsInputInputState {
this.value.current = "";
};

#onkeydown = (e: KeyboardEvent & { currentTarget: HTMLInputElement }) => {
#onkeydown: KeyboardEventHandler<HTMLInputElement> = (e) => {
if (e.key === kbd.ENTER) {
const valid = this.#root.addValue(e.currentTarget.value);
if (valid) this.#resetValue();
Expand All @@ -611,7 +687,7 @@ class TagsInputInputState {
}
};

#onpaste = (e: ClipboardEvent & { currentTarget: HTMLInputElement }) => {
#onpaste: ClipboardEventHandler<HTMLInputElement> = (e) => {
if (!e.clipboardData || this.#pasteBehavior.current === "none") return;
const rawClipboardData = e.clipboardData.getData("text/plain");
// we're splitting this by the delimiters
Expand Down Expand Up @@ -663,7 +739,7 @@ class TagsInputClearState {
});
}

#onclick = () => {
#onclick: MouseEventHandler<HTMLButtonElement> = () => {
this.#root.clearValue();
};

Expand Down Expand Up @@ -704,7 +780,7 @@ class TagsInputTagContentState {
return undefined;
});

#ondblclick = (e: MouseEvent) => {
#ondblclick: MouseEventHandler<HTMLElement> = (e) => {
if (!this.tag.isEditable) return;
const target = e.target as HTMLElement;
if (this.tag.removeNode && isOrContainsTarget(this.tag.removeNode, target)) {
Expand Down Expand Up @@ -834,6 +910,10 @@ export function useTagsInputTagText(props: TagsInputTagTextStateProps) {
return new TagsInputTagTextState(props, getTagsInputTagContext());
}

export function useTagsInputTagEditInput(props: TagsInputTagEditInputStateProps) {
return new TagsInputTagEditInputState(props, getTagsInputTagContext());
}

export function useTagsInputTagEdit(props: TagsInputTagEditStateProps) {
return new TagsInputTagEditState(props, getTagsInputTagContext());
}
Expand Down
11 changes: 8 additions & 3 deletions packages/bits-ui/src/lib/bits/tags-input/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,14 @@ export type TagsInputTagRemoveProps = TagsInputTagRemovePropsWithoutHTML &

export type TagsInputTagEditPropsWithoutHTML = WithChild;

export type TagsInputTagEditProps = Omit<
TagsInputTagEditPropsWithoutHTML &
Without<BitsPrimitiveInputAttributes, TagsInputTagEditPropsWithoutHTML>,
export type TagsInputTagEditProps = TagsInputTagEditPropsWithoutHTML &
Without<BitsPrimitiveButtonAttributes, TagsInputTagEditPropsWithoutHTML>;

export type TagsInputTagEditInputPropsWithoutHTML = WithChild;

export type TagsInputTagEditInputProps = Omit<
TagsInputTagEditInputPropsWithoutHTML &
Without<BitsPrimitiveInputAttributes, TagsInputTagEditInputPropsWithoutHTML>,
"children"
>;

Expand Down
Loading

0 comments on commit 66e4efe

Please sign in to comment.