Skip to content

Commit

Permalink
next: Expose onPaste prop for PinInput (#685)
Browse files Browse the repository at this point in the history
  • Loading branch information
huntabyte authored Sep 28, 2024
1 parent ee240a1 commit de01287
Show file tree
Hide file tree
Showing 11 changed files with 76 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
value = $bindable(""),
onValueChange = noop,
controlledValue = false,
onPaste,
...restProps
}: RootProps = $props();
Expand Down Expand Up @@ -52,6 +53,7 @@
}
),
pushPasswordManagerStrategy: box.with(() => pushPasswordManagerStrategy),
onPaste: box.with(() => onPaste),
});
const mergedInputProps = $derived(mergeProps(restProps, rootState.inputProps));
Expand Down
16 changes: 11 additions & 5 deletions packages/bits-ui/src/lib/bits/pin-input/pin-input.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type PinInputRootStateProps = WithRefProps<
disabled: boolean;
// eslint-disable-next-line ts/no-explicit-any
onComplete: (...args: any[]) => void;
onPaste?: (text: string) => string;

// eslint-disable-next-line ts/no-explicit-any
pattern: any;
maxLength: number;
Expand All @@ -52,6 +54,7 @@ class PinInputRootState {
#mirrorSelectionStart = $state<number | null>(null);
#mirrorSelectionEnd = $state<number | null>(null);
#onComplete: PinInputRootStateProps["onComplete"];
#onPaste: PinInputRootStateProps["onPaste"];
value: PinInputRootStateProps["value"];
#previousValue = new Previous(() => this.value.current ?? "");
#maxLength: PinInputRootStateProps["maxLength"];
Expand Down Expand Up @@ -85,6 +88,7 @@ class PinInputRootState {
this.#autocomplete = props.autocomplete;
this.#inputmode = props.inputmode;
this.#inputId = props.inputId;
this.#onPaste = props.onPaste;

this.#initialLoad = {
value: this.value,
Expand Down Expand Up @@ -131,11 +135,11 @@ class PinInputRootState {
unsub = addEventListener(
document,
"selectionchange",
this.onDocumentSelectionChange,
this.#onDocumentSelectionChange,
{ capture: true }
);

this.onDocumentSelectionChange();
this.#onDocumentSelectionChange();
if (document.activeElement === input) {
this.#isFocused.current = true;
}
Expand Down Expand Up @@ -285,7 +289,7 @@ class PinInputRootState {
}
};

onDocumentSelectionChange = () => {
#onDocumentSelectionChange = () => {
const input = this.#inputRef.current;
const container = this.#ref.current;
if (!input || !container) return;
Expand Down Expand Up @@ -388,6 +392,8 @@ class PinInputRootState {
const content = e.clipboardData.getData("text/plain");
e.preventDefault();

const sanitizedContent = this.#onPaste?.current?.(content) ?? content;

const start = input.selectionStart === null ? undefined : input.selectionStart;
const end = input.selectionEnd === null ? undefined : input.selectionEnd;

Expand All @@ -396,8 +402,8 @@ class PinInputRootState {
const initNewVal = this.value.current;

const newValueUncapped = isReplacing
? initNewVal.slice(0, start) + content + initNewVal.slice(end)
: initNewVal.slice(0, start) + content + initNewVal.slice(start);
? initNewVal.slice(0, start) + sanitizedContent + initNewVal.slice(end)
: initNewVal.slice(0, start) + sanitizedContent + initNewVal.slice(start);

const newValue = newValueUncapped.slice(0, this.#maxLength.current);

Expand Down
9 changes: 9 additions & 0 deletions packages/bits-ui/src/lib/bits/pin-input/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ export type PinInputRootPropsWithoutHTML = Omit<
*/
onValueChange?: OnChangeFn<string>;

/**
* A callback function that is called when the user pastes text into the input.
* It receives the pasted text as an argument, and should return the sanitized text.
*
* Use this function to clean up the pasted text, like removing hyphens or other
* characters that should not make it into the input.
*/
onPaste?: (text: string) => string;

/**
* The max length of the input.
*/
Expand Down
27 changes: 27 additions & 0 deletions packages/bits-ui/src/tests/pin-input/pin-input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,31 @@ describe("pin Input", () => {
}
expect(mockComplete).toHaveBeenCalledTimes(2);
});

it("should handle paste events correctly", async () => {
const mockComplete = vi.fn();
const { user, hiddenInput } = setup({
onComplete: mockComplete,
});

await user.click(hiddenInput);
await user.paste("123456");

expect(mockComplete).toHaveBeenCalledTimes(1);
expect(mockComplete).toHaveBeenCalledWith("123456");
});

it("should allow the user to sanitize pasted text (remove hyphens, etc.)", async () => {
const mockComplete = vi.fn();
const { user, hiddenInput } = setup({
onComplete: mockComplete,
onPaste: (text) => text.replace(/-/g, ""),
});

await user.click(hiddenInput);
await user.paste("123-456");

expect(mockComplete).toHaveBeenCalledTimes(1);
expect(mockComplete).toHaveBeenCalledWith("123456");
});
});
14 changes: 14 additions & 0 deletions sites/docs/content/components/pin-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,18 @@ This structure allows for a seamless user experience while providing developers
</PinInput.Root>
```

## Paste Handling

The `onPaste` prop allows you to sanitize pasted text. This can be useful for cleaning up pasted text, like removing hyphens or other characters that should not make it into the input. This function should return the sanitized text.

```svelte
<script lang="ts">
import { PinInput } from "bits-ui";
</script>
<PinInput.Root onPaste={(text) => text.replace(/-/g, "")}>
<!-- ... -->
</PinInput.Root>
```

<APISection {schemas} />
2 changes: 1 addition & 1 deletion sites/docs/src/lib/components/demo-code-tabs.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
{#if expandable}
<button
class={cn(
"inline-flex select-none items-center justify-center whitespace-nowrap rounded-[7px] px-2.5 py-1.5 text-sm text-foreground ring-offset-background transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background"
"hidden select-none items-center justify-center whitespace-nowrap rounded-[7px] px-2.5 py-1.5 text-sm text-foreground ring-offset-background transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background sm:inline-flex"
)}
onclick={() => (open = !open)}
aria-label="Toggle code expansion"
Expand Down
2 changes: 1 addition & 1 deletion sites/docs/src/lib/components/demos/pin-input-demo.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"focus-override",
"relative h-14 w-10 text-[2rem]",
"flex items-center justify-center",
"transition-all duration-200",
"transition-all duration-75",
"border-y border-r border-foreground/20 first:rounded-l-md first:border-l last:rounded-r-md",
"text-foreground group-focus-within/pininput:border-foreground/40 group-hover/pininput:border-foreground/40",
"outline outline-0",
Expand Down
2 changes: 1 addition & 1 deletion sites/docs/src/lib/components/search.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
}}
>
<Dialog.Trigger
class="relative inline-flex h-10 items-center justify-center gap-3 whitespace-nowrap rounded-[9px] bg-foreground/5 px-4 text-sm font-semibold text-foreground text-foreground/80 ring-offset-background transition-colors hover:bg-dark-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background"
class="relative inline-flex h-10 items-center justify-center gap-3 whitespace-nowrap rounded-[9px] bg-foreground/5 px-4 text-sm font-medium text-foreground text-foreground/80 ring-offset-background transition-colors hover:bg-dark-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
Search Docs
<kbd
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { default as PinInputRootPushPasswordManagerStrategyProp } from "./pin-in
export { default as PinInputCellCellProp } from "./pin-input-cell-cell-prop.md";
export { default as PinInputRootChildSnippetProps } from "./pin-input-root-child-snippet-props.md";
export { default as PinInputRootChildrenSnippetProps } from "./pin-input-root-children-snippet-props.md";
export { default as PinInputRootOnPasteProp } from "./pin-input-root-on-paste.md";
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```ts
(pastedText: string) => string;
```
6 changes: 6 additions & 0 deletions sites/docs/src/lib/content/api-reference/pin-input.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
PinInputRootChildSnippetProps,
PinInputRootChildrenSnippetProps,
PinInputRootOnCompleteProp,
PinInputRootOnPasteProp,
PinInputRootPushPasswordManagerStrategyProp,
PinInputRootTextAlignProp,
} from "./extended-types/pin-input/index.js";
Expand Down Expand Up @@ -54,6 +55,11 @@ const root = createApiSchema<PinInputRootPropsWithoutHTML>({
description: "A callback function that is called when the input is completely filled.",
definition: PinInputRootOnCompleteProp,
}),
onPaste: createFunctionProp({
description:
"A callback function that is called when the user pastes text into the input. It receives the pasted text as an argument and should return the sanitized text. Useful for cleaning up pasted text, like removing hyphens or other characters that should not make it into the input.",
definition: PinInputRootOnPasteProp,
}),
inputId: createStringProp({
description: "Optionally provide an ID to apply to the hidden input element.",
}),
Expand Down

0 comments on commit de01287

Please sign in to comment.