diff --git a/.changeset/fruity-singers-rule.md b/.changeset/fruity-singers-rule.md new file mode 100644 index 000000000..db4a9dc80 --- /dev/null +++ b/.changeset/fruity-singers-rule.md @@ -0,0 +1,5 @@ +--- +"bits-ui": patch +--- + +fix(Pin Input): keyboard navigation 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 ef6561c30..dcb461e03 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 @@ -350,14 +350,14 @@ export class PinInputRootState { } else if (maxLength > 1 && val.length > 1) { let offset = 0; if (prev[0] !== null && prev[1] !== null) { - direction = c < prev[0] ? "backward" : "forward"; + direction = c < prev[1] ? "backward" : "forward"; const wasPreviouslyInserting = prev[0] === prev[1] && prev[0] < maxLength; if (direction === "backward" && !wasPreviouslyInserting) { offset = -1; } } - start = offset - c; + start = offset + c; end = offset + c + 1; } } diff --git a/tests/src/tests/pin-input/pin-input.browser.test.ts b/tests/src/tests/pin-input/pin-input.browser.test.ts index 4c9e4e43a..07a9b6197 100644 --- a/tests/src/tests/pin-input/pin-input.browser.test.ts +++ b/tests/src/tests/pin-input/pin-input.browser.test.ts @@ -3,7 +3,7 @@ import { render } from "vitest-browser-svelte"; import { REGEXP_ONLY_DIGITS } from "bits-ui"; import { getTestKbd } from "../utils.js"; import PinInputTest from "./pin-input-test.svelte"; -import { type ComponentProps } from "svelte"; +import type { ComponentProps } from "svelte"; import { page, userEvent } from "@vitest/browser/context"; const kbd = getTestKbd(); @@ -220,3 +220,116 @@ it("should allow pasting more than the max-length if transformation is provided" expect(mockComplete).toHaveBeenCalledTimes(1); expect(mockComplete).toHaveBeenCalledWith("123456"); }); + +it("should handle ArrowLeft navigation correctly", async () => { + const t = setup(); + + await t.hiddenInput.click(); + await userEvent.keyboard("1"); + await userEvent.keyboard("2"); + await userEvent.keyboard("3"); + await userEvent.keyboard("4"); + await expect.element(t.hiddenInput).toHaveValue("1234"); + await expect.element(t.cells[4]).toHaveAttribute("data-active"); + + await userEvent.keyboard(kbd.ARROW_LEFT); + await expect.element(t.cells[3]).toHaveAttribute("data-active"); + await expect.element(t.cells[4]).not.toHaveAttribute("data-active"); + await expect.element(t.cells[2]).not.toHaveAttribute("data-active"); + + await userEvent.keyboard(kbd.ARROW_LEFT); + await expect.element(t.cells[2]).toHaveAttribute("data-active"); + await expect.element(t.cells[3]).not.toHaveAttribute("data-active"); + + await userEvent.keyboard(kbd.ARROW_LEFT); + await expect.element(t.cells[1]).toHaveAttribute("data-active"); + await expect.element(t.cells[2]).not.toHaveAttribute("data-active"); + + await userEvent.keyboard(kbd.ARROW_LEFT); + await expect.element(t.cells[0]).toHaveAttribute("data-active"); + await expect.element(t.cells[1]).not.toHaveAttribute("data-active"); +}); + +it("should correctly replace characters when navigating back and typing", async () => { + const t = setup(); + + await t.hiddenInput.click(); + await t.hiddenInput.fill("1234"); + await expect.element(t.hiddenInput).toHaveValue("1234"); + + // navigate to beginning using Home + await userEvent.keyboard(kbd.HOME); + await expect.element(t.cells[0]).toHaveAttribute("data-active"); + + // type 5 and 6 - should replace first two characters + await userEvent.keyboard("5"); + await expect.element(t.hiddenInput).toHaveValue("5234"); + await expect.element(t.cells[0]).toHaveTextContent("5"); + await expect.element(t.cells[1]).toHaveAttribute("data-active"); + + await userEvent.keyboard("6"); + await expect.element(t.hiddenInput).toHaveValue("5634"); + await expect.element(t.cells[0]).toHaveTextContent("5"); + await expect.element(t.cells[1]).toHaveTextContent("6"); + await expect.element(t.cells[2]).toHaveTextContent("3"); + await expect.element(t.cells[3]).toHaveTextContent("4"); +}); + +it("should correctly replace characters when navigating back with ArrowLeft and typing", async () => { + const t = setup(); + + await t.hiddenInput.click(); + await t.hiddenInput.fill("1234"); + await expect.element(t.hiddenInput).toHaveValue("1234"); + + // navigate back using ArrowLeft + await userEvent.keyboard(kbd.ARROW_LEFT); + await expect.element(t.cells[3]).toHaveAttribute("data-active"); + + await userEvent.keyboard(kbd.ARROW_LEFT); + await expect.element(t.cells[2]).toHaveAttribute("data-active"); + + await userEvent.keyboard(kbd.ARROW_LEFT); + await expect.element(t.cells[1]).toHaveAttribute("data-active"); + + await userEvent.keyboard(kbd.ARROW_LEFT); + await expect.element(t.cells[0]).toHaveAttribute("data-active"); + + // type 5 and 6 - should replace first two characters + await userEvent.keyboard("5"); + await expect.element(t.hiddenInput).toHaveValue("5234"); + await expect.element(t.cells[0]).toHaveTextContent("5"); + await expect.element(t.cells[1]).toHaveAttribute("data-active"); + + await userEvent.keyboard("6"); + await expect.element(t.hiddenInput).toHaveValue("5634"); + await expect.element(t.cells[0]).toHaveTextContent("5"); + await expect.element(t.cells[1]).toHaveTextContent("6"); + await expect.element(t.cells[2]).toHaveTextContent("3"); + await expect.element(t.cells[3]).toHaveTextContent("4"); +}); + +it("should correctly replace characters when navigating with ArrowUp and typing", async () => { + const t = setup(); + + await t.hiddenInput.click(); + await t.hiddenInput.fill("1234"); + await expect.element(t.hiddenInput).toHaveValue("1234"); + + // navigate to beginning using ArrowUp (equivalent to Home) + await userEvent.keyboard(kbd.ARROW_UP); + await expect.element(t.cells[0]).toHaveAttribute("data-active"); + + // type 5 and 6 - should replace first two characters + await userEvent.keyboard("5"); + await expect.element(t.hiddenInput).toHaveValue("5234"); + await expect.element(t.cells[0]).toHaveTextContent("5"); + await expect.element(t.cells[1]).toHaveAttribute("data-active"); + + await userEvent.keyboard("6"); + await expect.element(t.hiddenInput).toHaveValue("5634"); + await expect.element(t.cells[0]).toHaveTextContent("5"); + await expect.element(t.cells[1]).toHaveTextContent("6"); + await expect.element(t.cells[2]).toHaveTextContent("3"); + await expect.element(t.cells[3]).toHaveTextContent("4"); +});