Skip to content

Commit

Permalink
feat: add pagination components (#223)
Browse files Browse the repository at this point in the history
Co-authored-by: Hunter Johnston <johnstonhuntera@gmail.com>
Co-authored-by: Hunter Johnston <64506580+huntabyte@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 7, 2023
1 parent e94129c commit ad6ba3a
Show file tree
Hide file tree
Showing 24 changed files with 610 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/breezy-coins-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"bits-ui": patch
---

feat: add `Pagination` component
33 changes: 33 additions & 0 deletions content/components/pagination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
title: Pagination
description: Facilitates navigation between pages.
---

<script>
import { APISection, ComponentPreview, PaginationDemo } from '@/components'
export let schemas
</script>

<ComponentPreview name="pagination-demo" comp="Pagination">

<PaginationDemo slot="preview" />

</ComponentPreview>

## Structure

```svelte
<script lang="ts">
import { Pagination } from "bits-ui";
</script>
<Pagination.Root let:pages>
<Pagination.PrevButton />
{#each pages as page (page.key)}
<Pagination.Page {page} />
{/each}
<Pagination.NextButton />
</Pagination.Root>
```

<APISection {schemas} />
1 change: 1 addition & 0 deletions src/components/demos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export { default as DropdownMenuDemo } from "./dropdown-menu-demo.svelte";
export { default as LabelDemo } from "./label-demo.svelte";
export { default as LinkPreviewDemo } from "./link-preview-demo.svelte";
export { default as MenubarDemo } from "./menubar-demo.svelte";
export { default as PaginationDemo } from "./pagination-demo.svelte";
export { default as PopoverDemo } from "./popover-demo.svelte";
export { default as ProgressDemo } from "./progress-demo.svelte";
export { default as RadioGroupDemo } from "./radio-group-demo.svelte";
Expand Down
36 changes: 36 additions & 0 deletions src/components/demos/pagination-demo.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script lang="ts">
import { Pagination } from "$lib";
import { CaretLeft, CaretRight, DotsThree } from "phosphor-svelte";
</script>

<Pagination.Root count={100} perPage={10} let:pages let:range>
<p class="text-sm text-muted-foreground">
Showing {range.start} - {range.end}
</p>
<div class="mt-4 flex items-center justify-center gap-1.5">
<Pagination.PrevButton
class="inline-flex items-center justify-center rounded-[9px] bg-transparent transition-all sq-10 hover:bg-foreground/5 active:scale-98 active:bg-dark-10 disabled:cursor-not-allowed disabled:opacity-50 hover:disabled:bg-transparent"
>
<CaretLeft class="sq-6" />
</Pagination.PrevButton>
{#each pages as page (page.key)}
{#if page.type === "ellipsis"}
<div class="self-end">
<DotsThree class="sq-5" weight="bold" />
</div>
{:else}
<Pagination.Page
{page}
class="inline-flex items-center justify-center rounded-[9px] border border-border bg-background shadow-mini sq-10 hover:bg-muted active:scale-98 active:bg-dark-10 data-[selected]:bg-foreground data-[selected]:text-background"
>
{page.value}
</Pagination.Page>
{/if}
{/each}
<Pagination.NextButton
class="inline-flex items-center justify-center rounded-[9px] bg-transparent transition-all sq-10 hover:bg-foreground/5 active:scale-98 active:bg-dark-10 disabled:cursor-not-allowed hover:disabled:bg-transparent"
>
<CaretRight class="sq-6" />
</Pagination.NextButton>
</div>
</Pagination.Root>
5 changes: 5 additions & 0 deletions src/config/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ export const navigation: Navigation = {
href: "/docs/components/menubar",
items: []
},
{
title: "Pagination",
href: "/docs/components/pagination",
items: []
},
{
title: "Popover",
href: "/docs/components/popover",
Expand Down
6 changes: 6 additions & 0 deletions src/content/api-reference/extended-types/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { PropType } from "@/types";
import rawFocusProp from "@/content/api-reference/extended-types/focus-target.js?raw";
import rawMonthProp from "@/content/api-reference/extended-types/months.js?raw";
import rawPageItemProp from "@/content/api-reference/extended-types/page-item.js?raw";

export const monthsPropType: PropType = {
type: "Month[]",
Expand All @@ -11,3 +12,8 @@ export const focusProp: PropType = {
type: "FocusProp",
definition: rawFocusProp
};

export const pageItemProp: PropType = {
type: "PageItem",
definition: rawPageItemProp
};
14 changes: 14 additions & 0 deletions src/content/api-reference/extended-types/page-item.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export type Page = {
type: "page";
/** The page number the `PageItem` represents */
value: number;
};

export type Ellipsis = {
type: "ellipsis";
};

export type PageItem = (Page | Ellipsis) & {
/** Unique key for the item, for svelte #each block */
key: string;
};
16 changes: 15 additions & 1 deletion src/content/api-reference/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export function portalProp(compName = "content") {
}

export function union(...types: string[]): string {
return types.join(" | ").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
return escape(types.join(" | "));
}

export function enums(...values: string[]): string {
Expand All @@ -102,3 +102,17 @@ export function enums(...values: string[]): string {
export function seeFloating(content: string, link: string) {
return `${content} [Floating UI reference](${link}).`;
}

const entities = [
[/</g, "&lt;"],
[/>/g, "&gt;"],
[/{/g, "&#123;"],
[/}/g, "&#125;"]
] as const;

export function escape(str: string): string {
for (let i = 0; i < entities.length; i += 1) {
str = str.replace(entities[i][0], entities[i][1]);
}
return str;
}
3 changes: 3 additions & 0 deletions src/content/api-reference/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { dropdownMenu } from "./dropdown-menu";
import { label } from "./label";
import { linkPreview } from "./link-preview";
import { popover } from "./popover";
import { pagination } from "./pagination";
import { menubar } from "./menubar";
import { progress } from "./progress";
import { radioGroup } from "./radio-group";
Expand Down Expand Up @@ -50,6 +51,7 @@ export const bits = [
"link-preview",
"label",
"menubar",
"pagination",
"popover",
"progress",
"radio-group",
Expand Down Expand Up @@ -92,6 +94,7 @@ export const apiSchemas: Record<Bit, APISchema[]> = {
label,
"link-preview": linkPreview,
menubar,
pagination,
popover,
progress,
"radio-group": radioGroup,
Expand Down
102 changes: 102 additions & 0 deletions src/content/api-reference/pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { APISchema } from "@/types";
import type * as Pagination from "$lib/bits/pagination/_types.js";
import * as C from "@/content/constants.js";
import { asChild } from "@/content/api-reference/helpers.js";
import { builderAndAttrsSlotProps } from "./helpers";
import { pageItemProp } from "./extended-types";

export const root: APISchema<Pagination.Props> = {
title: "Root",
description: "The root pagination component which contains all other pagination components.",
props: {
count: {
type: C.NUMBER,
description: "The total number of items.",
required: true
},
perPage: {
type: C.NUMBER,
description: "The number of items per page.",
default: "1"
},
siblingCount: {
type: C.NUMBER,
description: "The number of page triggers to show on either side of the current page.",
default: "1"
},
page: {
type: C.NUMBER,
description:
"The selected page. You can bind this to a variable to control the selected page from outside the component."
},
onPageChange: {
type: {
type: C.FUNCTION,
definition: "(page: number) => void"
},
description: "A function called when the selected page changes."
},
asChild
}
};

export const page: APISchema<Pagination.PageProps> = {
title: "Page",
description: "A button that triggers a page change.",
props: {
page: {
type: pageItemProp,
description: "The page item this component represents."
},
asChild
},
slotProps: {
...builderAndAttrsSlotProps
},
dataAttributes: [
{
name: "selected",
description: "Present on the current page element."
},
{
name: "pagination-page",
description: "Present on the page trigger element."
}
]
};

export const prevButton: APISchema<Pagination.PrevButtonProps> = {
title: "PrevButton",
description: "The previous button of the pagination.",
props: {
asChild
},
slotProps: {
...builderAndAttrsSlotProps
},
dataAttributes: [
{
name: "pagination-prev-button",
description: "Present on the previous button element."
}
]
};

export const nextButton: APISchema<Pagination.NextButtonProps> = {
title: "NextButton",
description: "The next button of the pagination.",
props: {
asChild
},
slotProps: {
...builderAndAttrsSlotProps
},
dataAttributes: [
{
name: "pagination-next-button",
description: "Present on the next button element."
}
]
};

export const pagination = [root, page, prevButton, nextButton];
1 change: 1 addition & 0 deletions src/lib/bits/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export * as DropdownMenu from "./dropdown-menu/index.js";
export * as LinkPreview from "./link-preview/index.js";
export * as Label from "./label/index.js";
export * as Menubar from "./menubar/index.js";
export * as Pagination from "./pagination/index.js";
export * as Popover from "./popover/index.js";
export * as Progress from "./progress/index.js";
export * as RadioGroup from "./radio-group/index.js";
Expand Down
11 changes: 11 additions & 0 deletions src/lib/bits/pagination/_export.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export type {
Props as PaginationProps,
PrevButtonProps as PaginationPrevButtonProps,
NextButtonProps as PaginationNextButtonProps,
PageProps as PaginationPageProps,
//
Events as PaginationEvents,
PrevButtonEvents as PaginationPrevButtonEvents,
NextButtonEvents as PaginationNextButtonEvents,
PageEvents as PaginationPageEvents
} from "./types.js";
36 changes: 36 additions & 0 deletions src/lib/bits/pagination/_types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* We define prop types without the HTMLAttributes here so that we can use them
* to type-check our API documentation, which requires we document each prop,
* but we don't want to document the HTML attributes.
*/

import type { AsChild, Expand, OnChangeFn } from "$lib/internal";
import type { CreatePaginationProps, Page } from "@melt-ui/svelte";

type OmitPaginationProps<T> = Omit<T, "page" | "defaultPage" | "onPageChange">;

type Props = Expand<
OmitPaginationProps<CreatePaginationProps> & {
/**
* The selected page. This updates as the users selects new pages.
*
* You can bind this to a value to programmatically control the value state.
*/
page?: number;

/**
* A callback function called when the page changes.
*/
onPageChange?: OnChangeFn<number>;
} & AsChild
>;

type PageProps = {
page: Page;
} & AsChild;

type PrevButtonProps = AsChild;

type NextButtonProps = AsChild;

export type { Props, PrevButtonProps, NextButtonProps, PageProps };
35 changes: 35 additions & 0 deletions src/lib/bits/pagination/components/pagination-next-button.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script lang="ts">
import type { NextButtonEvents, NextButtonProps } from "../types.js";
import { createDispatcher } from "$lib/internal/events.js";
import { getAttrs, getCtx } from "../ctx.js";
import { melt } from "@melt-ui/svelte";
type $$Props = NextButtonProps;
type $$Events = NextButtonEvents;
export let asChild: $$Props["asChild"] = undefined;
const {
elements: { nextButton }
} = getCtx();
const attrs = getAttrs("next-button");
$: builder = $nextButton;
$: Object.assign(builder, attrs);
const dispatch = createDispatcher();
</script>

{#if asChild}
<slot {builder} />
{:else}
<button
use:melt={builder}
type="button"
{...$$restProps}
on:m-click={dispatch}
>
<slot {builder} />
</button>
{/if}
Loading

0 comments on commit ad6ba3a

Please sign in to comment.