From 2f04f9a0ccb34c82bfecb0d8ea947658ded917b1 Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Mon, 3 Feb 2025 13:57:36 -0500 Subject: [PATCH 1/2] fix: pin input paste behavior --- .changeset/smooth-clouds-happen.md | 5 +++ .../lib/bits/pin-input/pin-input.svelte.ts | 41 +++++-------------- .../pin-input/usePasswordManager.svelte.ts | 18 -------- .../src/tests/pin-input/pin-input.test.ts | 20 ++++++++- 4 files changed, 34 insertions(+), 50 deletions(-) create mode 100644 .changeset/smooth-clouds-happen.md diff --git a/.changeset/smooth-clouds-happen.md b/.changeset/smooth-clouds-happen.md new file mode 100644 index 000000000..f358e1792 --- /dev/null +++ b/.changeset/smooth-clouds-happen.md @@ -0,0 +1,5 @@ +--- +"bits-ui": patch +--- + +fix: Pin Input paste behavior diff --git a/packages/bits-ui/src/lib/bits/pin-input/pin-input.svelte.ts b/packages/bits-ui/src/lib/bits/pin-input/pin-input.svelte.ts index 4c53a43b4..d91300b62 100644 --- a/packages/bits-ui/src/lib/bits/pin-input/pin-input.svelte.ts +++ b/packages/bits-ui/src/lib/bits/pin-input/pin-input.svelte.ts @@ -1,6 +1,6 @@ -import { Previous } from "runed"; +import { Previous, watch } from "runed"; import { untrack } from "svelte"; -import { type WritableBox, box, useRefById } from "svelte-toolbelt"; +import { type WritableBox, afterTick, box, useRefById } from "svelte-toolbelt"; import { usePasswordManagerBadge } from "./usePasswordManager.svelte.js"; import type { PinInputCell, PinInputRootProps as RootComponentProps } from "./types.js"; import type { ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box.svelte.js"; @@ -196,9 +196,7 @@ class PinInputRootState { }); }); - $effect(() => { - this.value.current; - this.#inputRef.current; + watch([() => this.value.current, () => this.#inputRef.current], () => { syncTimeouts(() => { const input = this.#inputRef.current; if (!input) return; @@ -427,34 +425,16 @@ class PinInputRootState { onpaste = (e: BitsEvent) => { const input = this.#inputRef.current; + if (!input) return; - if (!this.#initialLoad.isIOS) { - if (!e.clipboardData || !input) return; - const content = e.clipboardData.getData("text/plain"); - const sanitizedContent = this.#onPaste?.current?.(content) ?? content; - if ( - sanitizedContent.length > 0 && - this.#regexPattern && - !this.#regexPattern.test(sanitizedContent) - ) { - e.preventDefault(); - return; - } + if (!this.#onPaste?.current && (!this.#initialLoad.isIOS || !e.clipboardData || !input)) { + return; } - if (!this.#initialLoad.isIOS || !e.clipboardData || !input) return; - const content = e.clipboardData.getData("text/plain"); + const _content = e.clipboardData?.getData("text/plain") ?? ""; + const content = this.#onPaste?.current ? this.#onPaste.current(_content) : _content; e.preventDefault(); - const sanitizedContent = this.#onPaste?.current?.(content) ?? content; - if ( - sanitizedContent.length > 0 && - this.#regexPattern && - !this.#regexPattern.test(sanitizedContent) - ) { - return; - } - const start = input.selectionStart === null ? undefined : input.selectionStart; const end = input.selectionEnd === null ? undefined : input.selectionEnd; @@ -463,9 +443,8 @@ class PinInputRootState { const initNewVal = this.value.current; const newValueUncapped = isReplacing - ? initNewVal.slice(0, start) + sanitizedContent + initNewVal.slice(end) - : initNewVal.slice(0, start) + sanitizedContent + initNewVal.slice(start); - + ? initNewVal.slice(0, start) + content + initNewVal.slice(end) + : initNewVal.slice(0, start) + content + initNewVal.slice(start); const newValue = newValueUncapped.slice(0, this.#maxLength.current); if (newValue.length > 0 && this.#regexPattern && !this.#regexPattern.test(newValue)) { diff --git a/packages/bits-ui/src/lib/bits/pin-input/usePasswordManager.svelte.ts b/packages/bits-ui/src/lib/bits/pin-input/usePasswordManager.svelte.ts index a790578c0..043106fa3 100644 --- a/packages/bits-ui/src/lib/bits/pin-input/usePasswordManager.svelte.ts +++ b/packages/bits-ui/src/lib/bits/pin-input/usePasswordManager.svelte.ts @@ -27,11 +27,6 @@ export function usePasswordManagerBadge({ pushPasswordManagerStrategy, isFocused, }: UsePasswordManagerBadgeProps) { - let pwmMetadata = $state({ - done: false, - refocused: false, - }); - let hasPwmBadge = $state(false); let hasPwmBadgeSpace = $state(false); let done = $state(false); @@ -76,19 +71,6 @@ export function usePasswordManagerBadge({ hasPwmBadge = true; done = true; - - // for specific PWMs the input has to be re-focused - // to trigger a reposition of the badge - if (!pwmMetadata.refocused && document.activeElement === input) { - const selections = [input.selectionStart ?? 0, input.selectionEnd ?? 0]; - input.blur(); - input.focus(); - input.focus(); - // recover the previous selection - input.setSelectionRange(selections[0]!, selections[1]!); - - pwmMetadata.refocused = true; - } } $effect(() => { diff --git a/packages/tests/src/tests/pin-input/pin-input.test.ts b/packages/tests/src/tests/pin-input/pin-input.test.ts index 85261ef56..d4041357c 100644 --- a/packages/tests/src/tests/pin-input/pin-input.test.ts +++ b/packages/tests/src/tests/pin-input/pin-input.test.ts @@ -23,7 +23,7 @@ function setup(props: Partial = {}) { }; } -describe("pin Input", () => { +describe("Pin Input", () => { it("should have no accessibility violations", async () => { const { container } = render(PinInputTest); expect(await axe(container)).toHaveNoViolations(); @@ -198,4 +198,22 @@ describe("pin Input", () => { expect(mockComplete).toHaveBeenCalledTimes(0); }); + + it("should allow pasting more than the max-length if transformation is provided", async () => { + const mockComplete = vi.fn(); + const mockClipboard = "1-2-3-4-5-6"; + await navigator.clipboard.writeText(mockClipboard); + + const { user, hiddenInput } = setup({ + maxlength: 6, + onComplete: mockComplete, + onPaste: (text) => text.replace(/-/g, ""), + }); + + await user.click(hiddenInput); + await user.paste(mockClipboard); + + expect(mockComplete).toHaveBeenCalledTimes(1); + expect(mockComplete).toHaveBeenCalledWith("123456"); + }); }); From e633ee2ef3633a1ab0c8617e675081f44d9b4e67 Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Mon, 3 Feb 2025 14:01:40 -0500 Subject: [PATCH 2/2] lint --- packages/bits-ui/src/lib/bits/pin-input/pin-input.svelte.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bits-ui/src/lib/bits/pin-input/pin-input.svelte.ts b/packages/bits-ui/src/lib/bits/pin-input/pin-input.svelte.ts index d91300b62..f808d1cb9 100644 --- a/packages/bits-ui/src/lib/bits/pin-input/pin-input.svelte.ts +++ b/packages/bits-ui/src/lib/bits/pin-input/pin-input.svelte.ts @@ -1,6 +1,6 @@ import { Previous, watch } from "runed"; import { untrack } from "svelte"; -import { type WritableBox, afterTick, box, useRefById } from "svelte-toolbelt"; +import { type WritableBox, box, useRefById } from "svelte-toolbelt"; import { usePasswordManagerBadge } from "./usePasswordManager.svelte.js"; import type { PinInputCell, PinInputRootProps as RootComponentProps } from "./types.js"; import type { ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box.svelte.js";