From 9b7b2e651ecf9b6c19b2b24c0ce728ddd9520599 Mon Sep 17 00:00:00 2001 From: Preston Booth Date: Tue, 22 Sep 2026 07:40:25 -0600 Subject: [PATCH 1/5] refactor(mosaic): align pagination with table footers --- .changeset/mosaic-pagination-table-footer.md | 5 + .../pagination/pagination.styles.ts | 80 ++++-- .../components/pagination/pagination.test.tsx | 264 ++++-------------- .../src/components/pagination/pagination.tsx | 252 ++++++++--------- packages/swingset/src/lib/registry.ts | 4 +- packages/swingset/src/stories/pagination.mdx | 24 +- .../src/stories/pagination.stories.tsx | 6 +- 7 files changed, 246 insertions(+), 389 deletions(-) create mode 100644 .changeset/mosaic-pagination-table-footer.md diff --git a/.changeset/mosaic-pagination-table-footer.md b/.changeset/mosaic-pagination-table-footer.md new file mode 100644 index 00000000000..19157bfc0a0 --- /dev/null +++ b/.changeset/mosaic-pagination-table-footer.md @@ -0,0 +1,5 @@ +--- +'@clerk/mosaic': patch +--- + +Update Pagination with compact table-footer controls and item-range context. diff --git a/packages/mosaic/src/components/pagination/pagination.styles.ts b/packages/mosaic/src/components/pagination/pagination.styles.ts index 19e7e7bf259..7c92d45de40 100644 --- a/packages/mosaic/src/components/pagination/pagination.styles.ts +++ b/packages/mosaic/src/components/pagination/pagination.styles.ts @@ -2,47 +2,75 @@ import * as stylex from '@stylexjs/stylex'; import { colorVars, space, typeScaleVars } from '../../tokens.stylex'; +const compact = '@container (width < 48rem)' as const; + export const styles = stylex.create({ root: { gap: space['4'], - alignItems: 'center', + alignItems: { [compact]: 'stretch', default: 'center' }, display: 'flex', + flexDirection: { [compact]: 'column', default: 'row' }, flexWrap: 'wrap', justifyContent: 'space-between', }, - controls: { - gap: space['1'], + summary: { + gap: space['3'], alignItems: 'center', display: 'flex', + flexWrap: { [compact]: 'nowrap', default: 'wrap' }, + justifyContent: { [compact]: 'space-between', default: 'flex-start' }, + width: { [compact]: '100%', default: 'auto' }, }, - hitTarget: { - position: 'relative', - '::after': { - insetBlock: 0, - insetInline: `calc(${space['1']} / -2)`, - content: '""', - position: 'absolute', - }, - }, - page: { - color: { - default: colorVars['--cl-color-foreground-secondary'], - ':where([aria-current="page"])': colorVars['--cl-color-foreground'], - }, - }, - ellipsis: { + pageSize: { + gap: space['3'], alignItems: 'center', - color: colorVars['--cl-color-foreground-secondary'], - display: 'inline-flex', - justifyContent: 'center', - height: space['7'], - width: space['7'], + display: 'flex', }, - pageSize: { - gap: space['2'], + controls: { + gap: space['1'], + alignItems: 'center', + borderBlockStartColor: { [compact]: colorVars['--cl-color-border'], default: 'transparent' }, + borderBlockStartStyle: 'solid', + borderBlockStartWidth: { [compact]: '1px', default: 0 }, + display: { [compact]: 'grid', default: 'flex' }, + gridTemplateColumns: { [compact]: '1fr auto 1fr', default: 'none' }, + paddingBlockStart: { [compact]: space['4'], default: 0 }, + width: { [compact]: '100%', default: 'auto' }, + }, + controlGroup: { + gap: space['1'], alignItems: 'center', display: 'flex', }, + controlGroupEnd: { + justifySelf: 'end', + }, + text: { + color: colorVars['--cl-color-foreground-secondary'], + fontSize: typeScaleVars['--cl-text-sm-size'], + lineHeight: typeScaleVars['--cl-text-sm-leading'], + whiteSpace: 'nowrap', + }, + divider: { + alignSelf: 'center', + backgroundColor: colorVars['--cl-color-border'], + display: { [compact]: 'none', default: 'block' }, + height: '1.25rem', + width: '1px', + }, + pageSizeLabel: { + display: { [compact]: 'none', default: 'inline' }, + }, + pageSizeLabelCompact: { + display: { [compact]: 'inline', default: 'none' }, + }, + pageLabel: { + paddingInline: space['1'], + color: colorVars['--cl-color-foreground-secondary'], + fontSize: typeScaleVars['--cl-text-sm-size'], + lineHeight: typeScaleVars['--cl-text-sm-leading'], + whiteSpace: 'nowrap', + }, pageSizeOption: { paddingBlock: 0, paddingInline: space['2'], diff --git a/packages/mosaic/src/components/pagination/pagination.test.tsx b/packages/mosaic/src/components/pagination/pagination.test.tsx index e693b0d07e3..06f6bc94ce1 100644 --- a/packages/mosaic/src/components/pagination/pagination.test.tsx +++ b/packages/mosaic/src/components/pagination/pagination.test.tsx @@ -5,7 +5,6 @@ import React from 'react'; import { describe, expect, it, vi } from 'vitest'; import { rtl } from '../../utils/rtl.styles'; -import { getPageItems } from './page-items'; import { Pagination } from './pagination'; import { styles } from './pagination.styles'; @@ -21,45 +20,19 @@ function controls(): ReturnType { return within(element); } -describe('getPageItems', () => { - it('shows every page when they all fit', () => { - expect(getPageItems(1, 2, 1)).toEqual([1, 2]); - expect(getPageItems(2, 3, 1)).toEqual([1, 2, 3]); - expect(getPageItems(1, 5, 1)).toEqual([1, 2, 3, 4, 5]); - expect(getPageItems(1, 7, 1)).toEqual([1, 2, 3, 4, 5, 6, 7]); - }); - - it('keeps the sequence the same length as the page moves', () => { - expect(getPageItems(1, 10, 1)).toEqual([1, 2, 3, 4, 5, 'end-ellipsis', 10]); - expect(getPageItems(10, 10, 1)).toEqual([1, 'start-ellipsis', 6, 7, 8, 9, 10]); - }); - - it('keeps the current page, its siblings, and both ends', () => { - expect(getPageItems(5, 10, 1)).toEqual([1, 'start-ellipsis', 4, 5, 6, 'end-ellipsis', 10]); - expect(getPageItems(5, 10, 2)).toEqual([1, 2, 3, 4, 5, 6, 7, 'end-ellipsis', 10]); - }); - - it('shows the page instead of an ellipsis that would hide only one', () => { - expect(getPageItems(4, 10, 1)).toEqual([1, 2, 3, 4, 5, 'end-ellipsis', 10]); - expect(getPageItems(7, 10, 1)).toEqual([1, 'start-ellipsis', 6, 7, 8, 9, 10]); - }); - - it('renders a single page', () => { - expect(getPageItems(1, 1, 1)).toEqual([1]); - }); -}); - describe('Mosaic Pagination', () => { - it('renders a labelled nav with the styling contract', () => { + it('renders the item range, page count, and styling contract', () => { render( , ); const nav = screen.getByRole('navigation', { name: 'Pagination' }); expect(nav).toHaveClass('cl-pagination'); + expect(screen.getByText('21–30 of 44')).toHaveClass('cl-pagination-range'); + expect(screen.getByText('3/5')).toHaveClass('cl-pagination-page-label'); }); it('uses the label prop as the accessible name', () => { @@ -74,65 +47,39 @@ describe('Mosaic Pagination', () => { expect(screen.getByRole('navigation', { name: 'Invoices' })).toBeInTheDocument(); }); - it('derives the page count from totalItems and pageSize', () => { - render( - , - ); - const pages = controls() - .getAllByRole('button') - .map(button => button.textContent) - .filter(Boolean); - expect(pages).toEqual(['1', '2', '3', '4', '5', '10']); - }); - - it('marks the current page', () => { - render( - , - ); - expect(screen.getByRole('button', { name: '3' })).toHaveAttribute('aria-current', 'page'); - expect(screen.getByRole('button', { name: '2' })).not.toHaveAttribute('aria-current'); - }); - - it('calls onChange with the clicked page', async () => { - const onChange = vi.fn(); + it('retains the sibling count styling contract', () => { render( , ); - await userEvent.click(screen.getByRole('button', { name: '2' })); - expect(onChange).toHaveBeenCalledWith(2); + expect(screen.getByRole('navigation')).toHaveAttribute('data-sibling-count', '2'); }); - it('moves by step from the previous and next buttons', async () => { + it('moves with the first, previous, next, and last controls', async () => { const onChange = vi.fn(); render( , ); + await userEvent.click(screen.getByRole('button', { name: 'First page' })); + expect(onChange).toHaveBeenLastCalledWith(1); await userEvent.click(screen.getByRole('button', { name: 'Previous page' })); - expect(onChange).toHaveBeenLastCalledWith(3); + expect(onChange).toHaveBeenLastCalledWith(4); await userEvent.click(screen.getByRole('button', { name: 'Next page' })); - expect(onChange).toHaveBeenLastCalledWith(7); + expect(onChange).toHaveBeenLastCalledWith(6); + await userEvent.click(screen.getByRole('button', { name: 'Last page' })); + expect(onChange).toHaveBeenLastCalledWith(10); }); - it('clamps a step that would overshoot the ends', async () => { + it('moves by step and clamps at the ends', async () => { const onChange = vi.fn(); render( { onChange={onChange} />, ); + await userEvent.click(screen.getByRole('button', { name: 'Previous page' })); + expect(onChange).toHaveBeenLastCalledWith(4); await userEvent.click(screen.getByRole('button', { name: 'Next page' })); expect(onChange).toHaveBeenLastCalledWith(10); }); - it('disables previous on the first page and next on the last', () => { + it('disables backward controls on the first page and forward controls on the last', () => { const { rerender } = render( { pageSize={10} />, ); + expect(screen.getByRole('button', { name: 'First page' })).toBeDisabled(); expect(screen.getByRole('button', { name: 'Previous page' })).toBeDisabled(); expect(screen.getByRole('button', { name: 'Next page' })).toBeEnabled(); @@ -167,34 +117,20 @@ describe('Mosaic Pagination', () => { ); expect(screen.getByRole('button', { name: 'Previous page' })).toBeEnabled(); expect(screen.getByRole('button', { name: 'Next page' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Last page' })).toBeDisabled(); }); - it('renders first and last buttons only when asked', async () => { - const onChange = vi.fn(); - const { rerender } = render( + it('can omit the first and last controls', () => { + render( , ); expect(screen.queryByRole('button', { name: 'First page' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Last page' })).not.toBeInTheDocument(); - - rerender( - , - ); - await userEvent.click(screen.getByRole('button', { name: 'First page' })); - expect(onChange).toHaveBeenLastCalledWith(1); - await userEvent.click(screen.getByRole('button', { name: 'Last page' })); - expect(onChange).toHaveBeenLastCalledWith(10); }); it('disables every control when disabled', () => { @@ -203,7 +139,6 @@ describe('Mosaic Pagination', () => { page={5} totalItems={100} pageSize={10} - hasFirstLast disabled />, ); @@ -214,18 +149,7 @@ describe('Mosaic Pagination', () => { expect(screen.getByRole('combobox')).toBeDisabled(); }); - it('shows the page size in the results-per-page select', () => { - render( - , - ); - expect(screen.getByRole('combobox', { name: 'Results per page 25' })).toBeInTheDocument(); - }); - - it('calls onPageSizeChange with the chosen page size', async () => { + it('shows and changes the page size', async () => { const onPageSizeChange = vi.fn(); render( { onPageSizeChange={onPageSizeChange} />, ); - await userEvent.click(screen.getByRole('combobox')); - await userEvent.click(screen.getByRole('option', { name: '50' })); - expect(onPageSizeChange).toHaveBeenCalledWith(50); + const trigger = screen.getByRole('combobox', { name: /Results per page/ }); + expect(trigger).toHaveTextContent('10'); + await userEvent.click(trigger); + await userEvent.click(screen.getByRole('option', { name: '20' })); + expect(onPageSizeChange).toHaveBeenCalledWith(20); }); - it('uses the pageSizeLabel prop as the page size label', () => { - render( - , - ); - expect(screen.getByText('Rows per page')).toHaveClass('cl-text'); - expect(screen.getByRole('combobox', { name: 'Rows per page 10' })).toBeInTheDocument(); - }); - - it('uses the control label props as the control names', () => { + it('uses custom visible and accessible labels', () => { render( , ); + expect(screen.getByText('11 à 20 sur 100')).toHaveClass('cl-pagination-range'); + expect(screen.getByText('Rows per page')).toBeInTheDocument(); + expect(screen.getByText('Rows')).toBeInTheDocument(); + expect(screen.getByRole('combobox', { name: /Rows per page/ })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Start' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Back' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Forward' })).toBeInTheDocument(); @@ -278,7 +197,6 @@ describe('Mosaic Pagination', () => { page={2} totalItems={100} pageSize={10} - hasFirstLast />, ); const mirror = stylex.props(rtl.mirror).className ?? ''; @@ -287,120 +205,35 @@ describe('Mosaic Pagination', () => { } }); - it('extends every control hit target across the gap', () => { + it('uses small outlined navigation controls', () => { render( , ); - const hitTarget = stylex.props(styles.hitTarget).className ?? ''; - for (const name of ['First page', 'Previous page', '2', 'Next page', 'Last page']) { - expect(screen.getByRole('button', { name })).toHaveClass(hitTarget); + for (const button of controls().getAllByRole('button')) { + expect(button).toHaveAttribute('data-size', 'sm'); + expect(button).toHaveAttribute('data-variant', 'outline'); } }); - it('renders the page size label as secondary sm text', () => { - render( - , - ); - const label = screen.getByText('Results per page'); - expect(label).toHaveClass('cl-text', 'cl-pagination-label'); - expect(label).toHaveAttribute('data-size', 'sm'); - expect(label).toHaveAttribute('data-color', 'foreground-secondary'); - }); - - it('sizes the page size options to match the trigger', async () => { - render( - , - ); - await userEvent.click(screen.getByRole('combobox')); - expect(screen.getByRole('option', { name: '10' })).toHaveClass(stylex.props(styles.pageSizeOption).className ?? ''); - }); - - it('offers the given page sizes plus the current one', async () => { + it('offers valid given page sizes plus the current one', async () => { render( , ); await userEvent.click(screen.getByRole('combobox')); - expect(screen.getAllByRole('option').map(option => option.textContent)).toEqual(['10', '15', '20']); - }); - - it('drops invalid and duplicate page size options', async () => { - render( - , - ); - await userEvent.click(screen.getByRole('combobox')); - expect(screen.getAllByRole('option').map(option => option.textContent)).toEqual(['10', '12', '25']); - }); - - it('treats a page size below one as one', async () => { - const onChange = vi.fn(); - render( - , - ); - const pages = controls() - .getAllByRole('button') - .map(button => button.textContent) - .filter(Boolean); - expect(pages).toEqual(['1', '2', '3', '4', '5']); - await userEvent.click(controls().getByRole('button', { name: '5' })); - expect(onChange).toHaveBeenCalledWith(5); - }); - - it('treats a step below one as one', async () => { - const onChange = vi.fn(); - render( - , - ); - await userEvent.click(screen.getByRole('button', { name: 'Next page' })); - expect(onChange).toHaveBeenCalledWith(3); - }); - - it('treats a negative sibling count as zero', () => { - render( - , - ); - expect(controls().getByRole('button', { name: '5' })).toHaveAttribute('aria-current', 'page'); + expect(screen.getAllByRole('option').map(option => option.textContent)).toEqual(['12', '15', '20']); + expect(screen.getByRole('option', { name: '15' })).toHaveClass(stylex.props(styles.pageSizeOption).className ?? ''); }); - it('renders one page when there are no items', () => { + it('renders an empty range as a single disabled page', () => { render( { pageSize={10} />, ); - expect(screen.getByRole('button', { name: '1' })).toHaveAttribute('aria-current', 'page'); + expect(screen.getByText('0–0 of 0')).toBeInTheDocument(); + expect(screen.getByText('1/1')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Previous page' })).toBeDisabled(); expect(screen.getByRole('button', { name: 'Next page' })).toBeDisabled(); }); diff --git a/packages/mosaic/src/components/pagination/pagination.tsx b/packages/mosaic/src/components/pagination/pagination.tsx index dfbe55e1c5e..b383cf94bb0 100644 --- a/packages/mosaic/src/components/pagination/pagination.tsx +++ b/packages/mosaic/src/components/pagination/pagination.tsx @@ -1,16 +1,14 @@ import * as stylex from '@stylexjs/stylex'; import React from 'react'; +import { fill } from '../../localization/messages'; import type { MosaicElementProps } from '../../props'; import { mergeStyleProps, themeProps } from '../../props'; import { reset } from '../../utils/reset.styles'; import { rtl } from '../../utils/rtl.styles'; import { Button } from '../button'; -import { ButtonContext } from '../button/button.context'; import { Icon } from '../icon'; import { Select } from '../select'; -import { Text } from '../text'; -import { getPageItems } from './page-items'; import { styles } from './pagination.styles'; export interface PaginationProps extends Omit, 'onChange'> { @@ -25,7 +23,11 @@ export interface PaginationProps extends Omit, 'onChan siblingCount?: number; disabled?: boolean; label?: string; + /** The item range, with `{start}`, `{end}` and `{total}` filled in. */ + rangeLabel?: string; pageSizeLabel?: string; + /** Stands in for `pageSizeLabel` when the pagination is too narrow to carry it. */ + pageSizeLabelCompact?: string; firstPageLabel?: string; previousPageLabel?: string; nextPageLabel?: string; @@ -36,19 +38,15 @@ function atLeast(value: number, min: number): number { return Number.isFinite(value) && value > min ? Math.floor(value) : min; } -const defaultPageSizeOptions = [10, 25, 50, 100]; +const defaultPageSizeOptions = [10, 15, 20, 100]; /** - * Page navigation for a paged list. Renders a labelled `nav` with previous/next controls, the - * page numbers around the current one, and a results-per-page control. Pages are 1-based; + * Page navigation for a paged list. Renders the current item range, a results-per-page control, + * first/previous/next/last controls, and the current page over the total. Pages are 1-based; * `onChange` receives the page the user asked for. * * @example * - * - * @example - * // With jump-to-ends controls and a wider window of pages - * */ export const Pagination = React.forwardRef(function MosaicPagination( { @@ -58,12 +56,14 @@ export const Pagination = React.forwardRef(functio onChange, pageSizeOptions = defaultPageSizeOptions, onPageSizeChange, - hasFirstLast = false, + hasFirstLast = true, step = 1, siblingCount = 1, disabled = false, label = 'Pagination', + rangeLabel = '{start}–{end} of {total}', pageSizeLabel = 'Results per page', + pageSizeLabelCompact = 'Show', firstPageLabel = 'First page', previousPageLabel = 'Previous page', nextPageLabel = 'Next page', @@ -76,9 +76,11 @@ export const Pagination = React.forwardRef(functio const itemsPerPage = atLeast(pageSize, 1); const pageStep = atLeast(step, 1); const siblings = atLeast(siblingCount, 0); - const pageCount = Math.max(1, Math.ceil(atLeast(totalItems, 0) / itemsPerPage)); + const itemCount = atLeast(totalItems, 0); + const pageCount = Math.max(1, Math.ceil(itemCount / itemsPerPage)); const current = Math.min(atLeast(page, 1), pageCount); - const labelId = React.useId(); + const start = itemCount === 0 ? 0 : (current - 1) * itemsPerPage + 1; + const end = Math.min(current * itemsPerPage, itemCount); const pageSizeItems = React.useMemo(() => { const sizes = new Set( [...pageSizeOptions, itemsPerPage].filter(size => Number.isFinite(size) && size >= 1).map(Math.floor), @@ -87,164 +89,140 @@ export const Pagination = React.forwardRef(functio }, [pageSizeOptions, itemsPerPage]); const isFirst = current <= 1; const isLast = current >= pageCount; - - const pageDefaults = React.useMemo( - () => - ({ - color: 'neutral', - variant: 'ghost', - size: 'sm', - disabled, - styles: styles.page, - }) as const, - [disabled], - ); - const goTo = (next: number) => onChange?.(Math.min(Math.max(next, 1), pageCount)); return ( ); diff --git a/packages/swingset/src/lib/registry.ts b/packages/swingset/src/lib/registry.ts index 55b0511c7e3..9f8ff641cae 100644 --- a/packages/swingset/src/lib/registry.ts +++ b/packages/swingset/src/lib/registry.ts @@ -136,8 +136,8 @@ import { Disabled as PaginationDisabled, FirstLast as PaginationFirstLast, meta as paginationMeta, + MiddlePage as PaginationMiddlePage, Primary as PaginationPrimary, - Siblings as PaginationSiblings, SinglePage as PaginationSinglePage, } from '../stories/pagination.stories'; import { @@ -445,7 +445,7 @@ const paginationModule: StoryModule = { meta: paginationMeta, Primary: PaginationPrimary, FirstLast: PaginationFirstLast, - Siblings: PaginationSiblings, + MiddlePage: PaginationMiddlePage, SinglePage: PaginationSinglePage, Disabled: PaginationDisabled, }; diff --git a/packages/swingset/src/stories/pagination.mdx b/packages/swingset/src/stories/pagination.mdx index 3edae7ddb0f..394a1a8a6a0 100644 --- a/packages/swingset/src/stories/pagination.mdx +++ b/packages/swingset/src/stories/pagination.mdx @@ -2,7 +2,9 @@ import * as PaginationStories from './pagination.stories'; # Pagination -Page navigation for a paged list. Renders a labelled `nav` with previous and next controls, the page numbers around the current one, and a results-per-page control. Pages are 1-based and the component is controlled: `onChange` receives the page the user asked for, and `onPageSizeChange` receives the size chosen from the results-per-page `Select`. +Page navigation for a paged list. It shows the visible item range, a results-per-page control, +first and previous controls, the current page count, and next and last controls. Pages are 1-based +and controlled. ## Example @@ -20,17 +22,29 @@ Page navigation for a paged list. Renders a labelled `nav` with previous and nex { name: 'totalItems', type: 'number', default: '—', description: 'Total number of items across every page.' }, { name: 'pageSize', type: 'number', default: '—', description: 'Items per page. The page count is derived from this and `totalItems`.' }, { name: 'onChange', type: '(page: number) => void', default: '—', description: 'Called with the page the user navigated to.' }, - { name: 'pageSizeOptions', type: 'number[]', default: '[10, 25, 50, 100]', description: 'Sizes offered by the results-per-page select. The current `pageSize` is always included.' }, + { name: 'pageSizeOptions', type: 'number[]', default: '[10, 15, 20, 100]', description: 'Sizes offered by the results-per-page select. The current `pageSize` is always included.' }, { name: 'onPageSizeChange', type: '(pageSize: number) => void', default: '—', description: 'Called with the page size the user chose.' }, { name: 'step', type: 'number', default: '1', description: 'How many pages the previous and next controls move.' }, - { name: 'siblingCount', type: 'number', default: '1', description: 'Pages shown on each side of the current one.' }, + { name: 'siblingCount', type: 'number', default: '1', description: 'Retained for compatibility and reflected as `data-sibling-count`.' }, { name: 'label', type: 'string', default: "'Pagination'", description: 'Accessible name of the `nav`.' }, + { + name: 'rangeLabel', + type: 'string', + default: "'{start}–{end} of {total}'", + description: 'The item range, with `{start}`, `{end}` and `{total}` filled in.', + }, { name: 'pageSizeLabel', type: 'string', default: "'Results per page'", description: 'Text of the label beside the page size select.', }, + { + name: 'pageSizeLabelCompact', + type: 'string', + default: "'Show'", + description: 'Stands in for `pageSizeLabel` when the pagination is too narrow to carry it.', + }, { name: 'firstPageLabel', type: 'string', default: "'First page'", description: 'Accessible name of the first page control.' }, { name: 'previousPageLabel', @@ -62,10 +76,10 @@ Page navigation for a paged list. Renders a labelled `nav` with previous and nex storyModule={PaginationStories} /> -### Sibling count +### Middle page diff --git a/packages/swingset/src/stories/pagination.stories.tsx b/packages/swingset/src/stories/pagination.stories.tsx index b8a2755d379..dd370ecf088 100644 --- a/packages/swingset/src/stories/pagination.stories.tsx +++ b/packages/swingset/src/stories/pagination.stories.tsx @@ -13,7 +13,6 @@ export { default as __source } from './pagination.stories?raw'; export const meta: StoryMeta = { group: 'Components', status: 'wip', - substatus: 'needs design', title: 'Pagination', source: 'packages/mosaic/src/components/pagination/pagination.tsx', styles: { @@ -22,7 +21,7 @@ export const meta: StoryMeta = { disabled: { true: {}, false: {} }, }, _defaultVariants: { - hasFirstLast: false, + hasFirstLast: true, disabled: false, }, }, @@ -66,7 +65,7 @@ export function FirstLast(props: Record) { ); } -export function Siblings(props: Record) { +export function MiddlePage(props: Record) { const [page, setPage] = useState(10); return ( ) { totalItems={200} pageSize={10} onChange={setPage} - siblingCount={2} /> ); } From 7adc02c276e120270df46de8aafc2c9203b6aed6 Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Tue, 22 Sep 2026 12:02:27 -0600 Subject: [PATCH 2/5] fix(mosaic): give pagination its own query container --- .../pagination/pagination.styles.ts | 7 +- .../src/components/pagination/pagination.tsx | 184 +++++++++--------- 2 files changed, 99 insertions(+), 92 deletions(-) diff --git a/packages/mosaic/src/components/pagination/pagination.styles.ts b/packages/mosaic/src/components/pagination/pagination.styles.ts index 7c92d45de40..1b0423efbde 100644 --- a/packages/mosaic/src/components/pagination/pagination.styles.ts +++ b/packages/mosaic/src/components/pagination/pagination.styles.ts @@ -2,10 +2,15 @@ import * as stylex from '@stylexjs/stylex'; import { colorVars, space, typeScaleVars } from '../../tokens.stylex'; -const compact = '@container (width < 48rem)' as const; +const compact = '@container cl-pagination (width < 48rem)' as const; export const styles = stylex.create({ root: { + containerName: 'cl-pagination', + containerType: 'inline-size', + width: '100%', + }, + layout: { gap: space['4'], alignItems: { [compact]: 'stretch', default: 'center' }, display: 'flex', diff --git a/packages/mosaic/src/components/pagination/pagination.tsx b/packages/mosaic/src/components/pagination/pagination.tsx index b383cf94bb0..465bb9cb388 100644 --- a/packages/mosaic/src/components/pagination/pagination.tsx +++ b/packages/mosaic/src/components/pagination/pagination.tsx @@ -101,127 +101,129 @@ export const Pagination = React.forwardRef(functio rest, )} > -
- - {fill(rangeLabel, { start, end, total: itemCount })} - - -
- {pageSizeLabel} +
+
+ + {fill(rangeLabel, { start, end, total: itemCount })} + - {pageSizeLabelCompact} - - onPageSizeChange?.(Number(value))} - > - ( -
-
-
-
- {hasFirstLast ? ( +
+
+ {hasFirstLast ? ( + + ) : null} - ) : null} - -
- - {current}/{pageCount} - -
- - {hasFirstLast ? ( +
+ + {current}/{pageCount} + +
- ) : null} + {hasFirstLast ? ( + + ) : null} +
From 8f305c4e0ee92f661e5c8558c94af79eaae2b4a5 Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Tue, 22 Sep 2026 12:02:43 -0600 Subject: [PATCH 3/5] refactor(mosaic): simplify pagination styles --- .../src/components/pagination/pagination.styles.ts | 9 +-------- packages/mosaic/src/components/pagination/pagination.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/mosaic/src/components/pagination/pagination.styles.ts b/packages/mosaic/src/components/pagination/pagination.styles.ts index 1b0423efbde..ec6b3f4930b 100644 --- a/packages/mosaic/src/components/pagination/pagination.styles.ts +++ b/packages/mosaic/src/components/pagination/pagination.styles.ts @@ -24,7 +24,6 @@ export const styles = stylex.create({ display: 'flex', flexWrap: { [compact]: 'nowrap', default: 'wrap' }, justifyContent: { [compact]: 'space-between', default: 'flex-start' }, - width: { [compact]: '100%', default: 'auto' }, }, pageSize: { gap: space['3'], @@ -34,13 +33,12 @@ export const styles = stylex.create({ controls: { gap: space['1'], alignItems: 'center', - borderBlockStartColor: { [compact]: colorVars['--cl-color-border'], default: 'transparent' }, + borderBlockStartColor: colorVars['--cl-color-border'], borderBlockStartStyle: 'solid', borderBlockStartWidth: { [compact]: '1px', default: 0 }, display: { [compact]: 'grid', default: 'flex' }, gridTemplateColumns: { [compact]: '1fr auto 1fr', default: 'none' }, paddingBlockStart: { [compact]: space['4'], default: 0 }, - width: { [compact]: '100%', default: 'auto' }, }, controlGroup: { gap: space['1'], @@ -57,7 +55,6 @@ export const styles = stylex.create({ whiteSpace: 'nowrap', }, divider: { - alignSelf: 'center', backgroundColor: colorVars['--cl-color-border'], display: { [compact]: 'none', default: 'block' }, height: '1.25rem', @@ -71,10 +68,6 @@ export const styles = stylex.create({ }, pageLabel: { paddingInline: space['1'], - color: colorVars['--cl-color-foreground-secondary'], - fontSize: typeScaleVars['--cl-text-sm-size'], - lineHeight: typeScaleVars['--cl-text-sm-leading'], - whiteSpace: 'nowrap', }, pageSizeOption: { paddingBlock: 0, diff --git a/packages/mosaic/src/components/pagination/pagination.tsx b/packages/mosaic/src/components/pagination/pagination.tsx index 465bb9cb388..c01845e7746 100644 --- a/packages/mosaic/src/components/pagination/pagination.tsx +++ b/packages/mosaic/src/components/pagination/pagination.tsx @@ -185,7 +185,12 @@ export const Pagination = React.forwardRef(functio />
- + {current}/{pageCount}
From 7871fe3af4abd59601bd7f40eb6d31fa0a4678ee Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Tue, 22 Sep 2026 12:02:56 -0600 Subject: [PATCH 4/5] docs(swingset): widen pagination examples --- packages/swingset/src/stories/pagination.stories.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/swingset/src/stories/pagination.stories.tsx b/packages/swingset/src/stories/pagination.stories.tsx index dd370ecf088..8bab16bedbf 100644 --- a/packages/swingset/src/stories/pagination.stories.tsx +++ b/packages/swingset/src/stories/pagination.stories.tsx @@ -14,6 +14,7 @@ export const meta: StoryMeta = { group: 'Components', status: 'wip', title: 'Pagination', + layout: 'wide', source: 'packages/mosaic/src/components/pagination/pagination.tsx', styles: { _variants: { From 89c30307f807010a2a6a86f1930b97b0dfa17996 Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Tue, 22 Sep 2026 12:03:45 -0600 Subject: [PATCH 5/5] chore: use empty pagination changeset --- .changeset/mosaic-pagination-table-footer.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/.changeset/mosaic-pagination-table-footer.md b/.changeset/mosaic-pagination-table-footer.md index 19157bfc0a0..a845151cc84 100644 --- a/.changeset/mosaic-pagination-table-footer.md +++ b/.changeset/mosaic-pagination-table-footer.md @@ -1,5 +1,2 @@ --- -'@clerk/mosaic': patch --- - -Update Pagination with compact table-footer controls and item-range context.