diff --git a/frontend/src/components/ui/pagination.ts b/frontend/src/components/ui/pagination.ts index 91e8625964..b76bcd8719 100644 --- a/frontend/src/components/ui/pagination.ts +++ b/frontend/src/components/ui/pagination.ts @@ -41,23 +41,7 @@ export function calculatePages({ } /** - * Pagination - * - * Persists via a search param in the URL. Defaults to `page`, but can be set with the `name` attribute. - * - * Usage example: - * ```ts - * - * - * ``` - * - * You can have multiple paginations on one page by setting different names: - * ```ts - * - * - * - * - * ``` + * Displays navigation links for paginated content. * * @fires page-change {PageChangeEvent} */ @@ -157,6 +141,7 @@ export class Pagination extends LitElement { ]; searchParams = new SearchParamsController(this, (params) => { + if (this.disablePersist) return; const page = parsePage(params.get(this.name)); if (this._page !== page) { this.dispatchEvent( @@ -172,6 +157,9 @@ export class Pagination extends LitElement { @state() private _page = 1; + /** + * Current page + */ @property({ type: Number }) set page(page: number) { if (page !== this._page) { @@ -184,18 +172,41 @@ export class Pagination extends LitElement { return this._page; } + /** + * Name of search param in URL. + * You can have multiple pagination elements in one view by setting different names. + */ @property({ type: String }) name = "page"; + /** + * Total number of items in the collection. + */ @property({ type: Number }) totalCount = 0; + /** + * Size of the paginated set + * + * @TODO Rename to `pageSize` as to not confuse with Shoelace `size` + */ @property({ type: Number }) size = 10; + /** + * Display pagination as minimally functional controls (previous, current, and next) + * + * @TODO Switch to a more standard attribute + */ @property({ type: Boolean }) compact = false; + /** + * Disable persisting current page in the URL + */ + @property({ type: Boolean }) + disablePersist = false; + @state() private inputValue = ""; @@ -207,18 +218,22 @@ export class Pagination extends LitElement { super.connectedCallback(); } - async willUpdate(changedProperties: PropertyValues) { + async willUpdate( + changedProperties: PropertyValues & Map, + ) { if (changedProperties.has("totalCount") || changedProperties.has("size")) { this.calculatePages(); } - const parsedPage = parseFloat( - this.searchParams.searchParams.get(this.name) ?? "1", - ); - if (parsedPage != this._page) { - const page = parsePage(this.searchParams.searchParams.get(this.name)); - const constrainedPage = Math.max(1, Math.min(this.pages, page)); - this.onPageChange(constrainedPage, { dispatch: false }); + if (!this.disablePersist) { + const parsedPage = parseFloat( + this.searchParams.searchParams.get(this.name) ?? "1", + ); + if (parsedPage != this._page) { + const page = parsePage(this.searchParams.searchParams.get(this.name)); + const constrainedPage = Math.max(1, Math.min(this.pages, page)); + this.onPageChange(constrainedPage, { dispatch: false }); + } } // if page is out of bounds, clamp it & dispatch an event to re-fetch data @@ -230,8 +245,8 @@ export class Pagination extends LitElement { this.onPageChange(constrainedPage, { dispatch: true }); } - if (changedProperties.get("page") && this._page) { - this.inputValue = `${this._page}`; + if (changedProperties.get("_page")) { + this.inputValue = `${this.page}`; } } @@ -407,10 +422,14 @@ export class Pagination extends LitElement { } private setPage(page: number) { - if (page === 1) { - this.searchParams.delete(this.name); + if (!this.disablePersist) { + if (page === 1) { + this.searchParams.delete(this.name); + } else { + this.searchParams.set(this.name, page.toString()); + } } else { - this.searchParams.set(this.name, page.toString()); + this._page = page; } } diff --git a/frontend/src/stories/components/Pagination.stories.ts b/frontend/src/stories/components/Pagination.stories.ts new file mode 100644 index 0000000000..6c19a925a1 --- /dev/null +++ b/frontend/src/stories/components/Pagination.stories.ts @@ -0,0 +1,51 @@ +import type { Meta, StoryObj } from "@storybook/web-components"; +import { html } from "lit"; + +import { renderComponent, type RenderProps } from "./Pagination"; + +const meta = { + title: "Components/Pagination", + component: "btrix-pagination", + tags: ["autodocs"], + decorators: (story) => + html`
${story()}
`, + render: renderComponent, + argTypes: { + searchParams: { table: { disable: true } }, + }, + args: { + totalCount: 10, + page: 1, + size: 1, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Basic: Story = { + args: {}, +}; + +/** + * By default, the current page persists between page reloads via a search param in the URL. + * You can disable pagination persistence by setting `disablePersist`. + */ +export const DisablePersistence: Story = { + args: { + disablePersist: true, + }, +}; + +/** + * Pagination can be displayed with a reduced amount of controls to fit a smaller visual space. + * Only the controls for the previous, current, and next page will be visible. Users can jump + * to a page by entering the page number in the input field for the current page. + * + * This should be used sparingly, such as for paginating secondary content in a view. + */ +export const Compact: Story = { + args: { + compact: true, + }, +}; diff --git a/frontend/src/stories/components/Pagination.ts b/frontend/src/stories/components/Pagination.ts new file mode 100644 index 0000000000..adb071205f --- /dev/null +++ b/frontend/src/stories/components/Pagination.ts @@ -0,0 +1,30 @@ +import { html } from "lit"; +import { ifDefined } from "lit/directives/if-defined.js"; + +import type { Pagination } from "@/components/ui/pagination"; + +import "@/components/ui/pagination"; + +export type RenderProps = Pagination; + +export const renderComponent = ({ + page, + name, + totalCount, + size, + compact, + disablePersist, +}: Partial) => { + return html` + + + `; +};