From 012057476d975624b8d2afc884633d75f9b4debf Mon Sep 17 00:00:00 2001 From: Alex Lohr Date: Wed, 8 Jul 2026 13:05:04 +0200 Subject: [PATCH 1/4] fix: pagination keyboard controls --- .storybook/ui/controls.tsx | 10 ++- packages/pagination/src/index.ts | 16 +++-- .../pagination/stories/pagination.stories.tsx | 65 ++++++++++--------- .../test/{index.test.ts => index.test.tsx} | 34 +++++++++- 4 files changed, 84 insertions(+), 41 deletions(-) rename packages/pagination/test/{index.test.ts => index.test.tsx} (85%) diff --git a/.storybook/ui/controls.tsx b/.storybook/ui/controls.tsx index 73aaaf553..42d469ab5 100644 --- a/.storybook/ui/controls.tsx +++ b/.storybook/ui/controls.tsx @@ -1,8 +1,11 @@ -import type { JSX } from "solid-js"; +import type { JSX } from "@solidjs/web"; import { colors, font, radii } from "./tokens.js"; export const Button = (props: { - onClick?: () => void; + "aria-current"?: "page"; + "aria-label"?: string; + onClick?: (ev: MouseEvent) => void; + onKeyUp?: (ev: KeyboardEvent) => void; children: JSX.Element; variant?: "primary" | "secondary" | "outline" | "ghost"; color?: string; @@ -12,9 +15,12 @@ export const Button = (props: { ref?: HTMLButtonElement | ((el: HTMLButtonElement) => void); }) => ( - ))} - - -
- - {([10, 20, 50] as const).map(n => ( - - ))} - -
- +

Error: {err}

}> + + + + +
+ + {([5, 7, 10] as const).map(n => ( + + ))} + +
+
+ + {([10, 20, 50] as const).map(n => ( + + ))} + +
+
+
); }, }); diff --git a/packages/pagination/test/index.test.ts b/packages/pagination/test/index.test.tsx similarity index 85% rename from packages/pagination/test/index.test.ts rename to packages/pagination/test/index.test.tsx index 5d22ec361..658681360 100644 --- a/packages/pagination/test/index.test.ts +++ b/packages/pagination/test/index.test.tsx @@ -1,5 +1,6 @@ import { describe, test, expect } from "vitest"; -import { createMemo, createRoot, createSignal, flush } from "solid-js"; +import { createMemo, createRoot, createSignal, flush, For } from "solid-js"; +import { render } from "@solidjs/web"; import { createInfiniteScroll, createPagination, @@ -193,6 +194,37 @@ describe("createPagination", () => { expect(page()).toBe(8); dispose(); }); + + test("sets the focus after going to a new page", async () => { + const originalFocus = HTMLElement.prototype.focus; + let current = document.body; + HTMLElement.prototype.focus = function() { + current = this; + originalFocus.call(this); + }; + const container = document.createElement('div'); + document.body.appendChild(container); + const Pagination = () => { + const [pagination] = createPagination({ pages: 10, maxPages: 5, initialPage: 1 }); + return ; + }; + const dispose = render(() => , container); + const activeButton = container?.querySelector('nav button[aria-current="page"]'); + activeButton instanceof HTMLElement && activeButton.focus(); + for (var i = 0; i < 5; i++) { + await new Promise(r => setTimeout(r, 15)); + current?.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, charCode: 0, code: "ArrowRight", key: "ArrowRight", keyCode: 39 })); + current?.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true, charCode: 0, code: "ArrowRight", key: "ArrowRight", keyCode: 39 })); + await new Promise(r => setTimeout(r, 45)); + expect(current.getAttribute("aria-current")).toBe("page"); + } + queueMicrotask(dispose); + HTMLElement.prototype.focus = originalFocus; + }); }); describe("createSegment", () => { From 48586314554a209cb26810dd26a0a332424a7fc6 Mon Sep 17 00:00:00 2001 From: Alex Lohr Date: Thu, 9 Jul 2026 09:34:37 +0200 Subject: [PATCH 2/4] fix: stories settings --- packages/pagination/src/index.ts | 13 ++-- .../pagination/stories/pagination.stories.tsx | 61 ++++++++++--------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/packages/pagination/src/index.ts b/packages/pagination/src/index.ts index ee1c30d4a..552753e23 100644 --- a/packages/pagination/src/index.ts +++ b/packages/pagination/src/index.ts @@ -168,14 +168,17 @@ export const createPagination = ( // Clamp page to valid range reactively — handles page count decreasing below current page const page: Accessor = createMemo(() => Math.max(1, Math.min(rawPage(), opts().pages))); - + const goPage = (p: number | ((p: number) => number), ev: KeyboardEvent) => { + // select the parent before we might get detached + let parent = ev.currentTarget, current; + while (parent instanceof HTMLElement && parent.childElementCount < 3) parent = parent.parentNode; setPage(p); - if (ev.currentTarget instanceof HTMLElement) { - let parent: HTMLElement | ParentNode | null = ev.currentTarget, current; - while (parent && !(current = parent.querySelector('[aria-current="page"]'))) + flush(); + // select the current page + if (parent) { + while (parent instanceof HTMLElement && !(current = parent.querySelector('[aria-current="page"]'))) parent = parent.parentNode; - current instanceof HTMLElement && current.focus(); } }; diff --git a/packages/pagination/stories/pagination.stories.tsx b/packages/pagination/stories/pagination.stories.tsx index 40f1059ce..82f6fcc6e 100644 --- a/packages/pagination/stories/pagination.stories.tsx +++ b/packages/pagination/stories/pagination.stories.tsx @@ -1,6 +1,6 @@ -import { createSignal, Errored, For, Show } from "solid-js"; +import { createMemo, createSignal, Errored, For, Show } from "solid-js"; import preview from "../../../.storybook/preview.js"; -import { createInfiniteScroll, createPagination, createSegment } from "../src/index.js"; +import { createInfiniteScroll, createPagination, createSegment, type PaginationOptions } from "../src/index.js"; import readme from "../README.md?raw"; import { Badge, @@ -30,30 +30,34 @@ const meta = preview.meta({ export default meta; const PaginationBar = (barProps: { - props: ReturnType[0]; + options: PaginationOptions; center?: boolean; -}) => ( - -); +}) => { + const [props, page] = createPagination(() => barProps.options); + return (<> + + + ); +} export const CreatePaginationStory = meta.story({ name: "createPagination", @@ -68,16 +72,15 @@ export const CreatePaginationStory = meta.story({ render: () => { const [totalPages, setTotalPages] = createSignal(20); const [maxVisible, setMaxVisible] = createSignal(7); - const [paginationProps, page, _setPage] = createPagination(() => ({ + const paginationOptions = createMemo(() => ({ pages: totalPages(), maxPages: maxVisible(), })); - + return (

Error: {err}

}> - - +
From 16d64ff91418b1694f404a32fbc940a9f7da9320 Mon Sep 17 00:00:00 2001 From: Alex Lohr Date: Thu, 9 Jul 2026 09:47:16 +0200 Subject: [PATCH 3/4] add changeset --- .changeset/bright-tigers-switch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/bright-tigers-switch.md diff --git a/.changeset/bright-tigers-switch.md b/.changeset/bright-tigers-switch.md new file mode 100644 index 000000000..30d00e8a0 --- /dev/null +++ b/.changeset/bright-tigers-switch.md @@ -0,0 +1,5 @@ +--- +"@solid-primitives/pagination": patch +--- + +fix keyboard focus loss From 2f8405f895dbb7d9754b9c4f6b0e96849970035f Mon Sep 17 00:00:00 2001 From: Alex Lohr Date: Thu, 9 Jul 2026 12:28:03 +0200 Subject: [PATCH 4/4] fix: linter warning --- packages/i18n/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/i18n/src/index.ts b/packages/i18n/src/index.ts index 14be362b5..d4a971c78 100644 --- a/packages/i18n/src/index.ts +++ b/packages/i18n/src/index.ts @@ -280,6 +280,7 @@ export function richText(string: string, tags: RichTextTags): JSX.Element { parts.push(render(inner)); } else { if (DEV) + // oxlint-disable-next-line no-console console.warn( `[@solid-primitives/i18n] richText: no renderer for tag "<${tag}>" was provided, rendering its contents as plain text.`, );