mirror of
https://github.com/carbon-design-system/carbon-components-svelte.git
synced 2025-09-15 02:11:05 +00:00
Merge pull request #215 from IBM/pagination-nav
feat: add PaginationNav component
This commit is contained in:
commit
2ae4c8a008
7 changed files with 244 additions and 0 deletions
18
src/PaginationNav/PaginationItem.svelte
Normal file
18
src/PaginationNav/PaginationItem.svelte
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<script>
|
||||||
|
export let page = 0;
|
||||||
|
export let active = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<li class:bx--pagination-nav__list-item={true}>
|
||||||
|
<button
|
||||||
|
data-page={page}
|
||||||
|
aria-current={active ? 'page' : undefined}
|
||||||
|
class:bx--pagination-nav__page={true}
|
||||||
|
class:bx--pagination-nav__page--active={active}
|
||||||
|
on:click>
|
||||||
|
<span class:bx--pagination-nav__accessibility-label={true}>
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
{page}
|
||||||
|
</button>
|
||||||
|
</li>
|
31
src/PaginationNav/PaginationNav.Story.svelte
Normal file
31
src/PaginationNav/PaginationNav.Story.svelte
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<script>
|
||||||
|
export let page = 0;
|
||||||
|
|
||||||
|
import PaginationNav from "./PaginationNav.svelte";
|
||||||
|
import { Button } from "../Button";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div style="width: 800px;">
|
||||||
|
<PaginationNav
|
||||||
|
{...$$props}
|
||||||
|
bind:page
|
||||||
|
on:change={({ detail }) => {
|
||||||
|
console.log('on:change', detail);
|
||||||
|
}}
|
||||||
|
on:click:button--previous={({ detail }) => {
|
||||||
|
console.log('button--previous', detail);
|
||||||
|
}}
|
||||||
|
on:click:button--next={({ detail }) => {
|
||||||
|
console.log('button--next', detail);
|
||||||
|
}} />
|
||||||
|
<div style="margin-top: 1.5rem">Bound page index: {page}</div>
|
||||||
|
<Button
|
||||||
|
kind="tertiary"
|
||||||
|
size="small"
|
||||||
|
style="margin-top: 1rem;"
|
||||||
|
on:click={() => {
|
||||||
|
page = 3;
|
||||||
|
}}>
|
||||||
|
Set page to index 3
|
||||||
|
</Button>
|
||||||
|
</div>
|
17
src/PaginationNav/PaginationNav.stories.js
Normal file
17
src/PaginationNav/PaginationNav.stories.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { withKnobs, number, boolean } from "@storybook/addon-knobs";
|
||||||
|
import Component from "./PaginationNav.Story.svelte";
|
||||||
|
|
||||||
|
export default { title: "PaginationNav", decorators: [withKnobs] };
|
||||||
|
|
||||||
|
export const Default = () => ({
|
||||||
|
Component,
|
||||||
|
props: {
|
||||||
|
page: number("Current page index (page)", 0),
|
||||||
|
total: number("Total number of items (total)", 10),
|
||||||
|
shown: number("Number of items to be shown (minimum 4) (shown)", 10),
|
||||||
|
loop: boolean(
|
||||||
|
"Allow user to loop through the items when reaching first / last (loop)",
|
||||||
|
false
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
129
src/PaginationNav/PaginationNav.svelte
Normal file
129
src/PaginationNav/PaginationNav.svelte
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
<script>
|
||||||
|
export let page = 0;
|
||||||
|
export let total = 10;
|
||||||
|
export let shown = 10;
|
||||||
|
export let loop = false;
|
||||||
|
export let forwardText = "Next page";
|
||||||
|
export let backwardText = "Previous page";
|
||||||
|
|
||||||
|
import { afterUpdate, createEventDispatcher } from "svelte";
|
||||||
|
import CaretLeft16 from "carbon-icons-svelte/lib/CaretLeft16";
|
||||||
|
import CaretRight16 from "carbon-icons-svelte/lib/CaretRight16";
|
||||||
|
import PaginationItem from "./PaginationItem.svelte";
|
||||||
|
import PaginationOverflow from "./PaginationOverflow.svelte";
|
||||||
|
import { Button } from "../Button";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
const MIN = 4; // minimum items shown
|
||||||
|
|
||||||
|
afterUpdate(() => {
|
||||||
|
dispatch("change", { page });
|
||||||
|
});
|
||||||
|
|
||||||
|
let front = 0;
|
||||||
|
let back = 0;
|
||||||
|
|
||||||
|
$: fit = shown >= MIN ? shown : MIN;
|
||||||
|
$: startOffset = fit <= MIN && page > 1 ? 0 : 1;
|
||||||
|
$: if (fit >= total) {
|
||||||
|
front = 0;
|
||||||
|
back = 0;
|
||||||
|
}
|
||||||
|
$: if (fit < total) {
|
||||||
|
const split = Math.ceil(fit / 2) - 1;
|
||||||
|
|
||||||
|
front = page - split + 1;
|
||||||
|
back = total - page - (fit - split) + 1;
|
||||||
|
|
||||||
|
if (front <= 1) {
|
||||||
|
back -= front <= 0 ? Math.abs(front) + 1 : 0;
|
||||||
|
front = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (back <= 1) {
|
||||||
|
front -= back <= 0 ? Math.abs(back) + 1 : 0;
|
||||||
|
back = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$: items = Array.from({ length: total })
|
||||||
|
.map((e, i) => i)
|
||||||
|
.slice(startOffset + front, (back + 1) * -1);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav aria-label="pagination" class:bx--pagination-nav={true} {...$$restProps}>
|
||||||
|
<ul class:bx--pagination-nav__list={true}>
|
||||||
|
<li class:bx--pagination-nav__list-item={true}>
|
||||||
|
<Button
|
||||||
|
hasIconOnly
|
||||||
|
kind="ghost"
|
||||||
|
tooltipAlignment="center"
|
||||||
|
tooltipPosition="bottom"
|
||||||
|
iconDescription={backwardText}
|
||||||
|
disabled={!loop && page === 0}
|
||||||
|
icon={CaretLeft16}
|
||||||
|
on:click={() => {
|
||||||
|
if (page - 1 < 0) {
|
||||||
|
if (loop) page = total - 1;
|
||||||
|
} else {
|
||||||
|
page--;
|
||||||
|
}
|
||||||
|
dispatch('click:button--previous', { page });
|
||||||
|
}} />
|
||||||
|
</li>
|
||||||
|
{#if fit > MIN || (fit <= MIN && page <= 1)}
|
||||||
|
<PaginationItem page={1} active={page === 0} on:click={() => (page = 0)}>
|
||||||
|
{page === 0 ? 'Active, Page' : 'Page'}
|
||||||
|
</PaginationItem>
|
||||||
|
{/if}
|
||||||
|
<PaginationOverflow
|
||||||
|
fromIndex={startOffset}
|
||||||
|
count={front}
|
||||||
|
on:select={({ detail }) => (page = detail.index)} />
|
||||||
|
{#each items as item}
|
||||||
|
<PaginationItem
|
||||||
|
page={item + 1}
|
||||||
|
active={page === item}
|
||||||
|
on:click={() => (page = item)}>
|
||||||
|
{page === item ? 'Active, Page' : 'Page'}
|
||||||
|
</PaginationItem>
|
||||||
|
{/each}
|
||||||
|
<PaginationOverflow
|
||||||
|
fromIndex={total - back - 1}
|
||||||
|
count={back}
|
||||||
|
on:select={({ detail }) => {
|
||||||
|
page = detail.index;
|
||||||
|
}} />
|
||||||
|
{#if total > 1}
|
||||||
|
<PaginationItem
|
||||||
|
page={total}
|
||||||
|
active={page === total - 1}
|
||||||
|
on:click={() => (page = total - 1)}>
|
||||||
|
{page === total - 1 ? 'Active, Page' : 'Page'}
|
||||||
|
</PaginationItem>
|
||||||
|
{/if}
|
||||||
|
<li class:bx--pagination-nav__list-item={true}>
|
||||||
|
<Button
|
||||||
|
hasIconOnly
|
||||||
|
kind="ghost"
|
||||||
|
tooltipAlignment="center"
|
||||||
|
tooltipPosition="bottom"
|
||||||
|
iconDescription={forwardText}
|
||||||
|
disabled={!loop && page === total - 1}
|
||||||
|
icon={CaretRight16}
|
||||||
|
on:click={() => {
|
||||||
|
if (page + 1 >= total) {
|
||||||
|
if (loop) page = 0;
|
||||||
|
} else {
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
dispatch('click:button--next', { page });
|
||||||
|
}} />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div
|
||||||
|
aria-live="polite"
|
||||||
|
aria-atomic="true"
|
||||||
|
class:bx--pagination-nav__accessibility-label={true}>
|
||||||
|
Page {page + 1} of {total}
|
||||||
|
</div>
|
||||||
|
</nav>
|
47
src/PaginationNav/PaginationOverflow.svelte
Normal file
47
src/PaginationNav/PaginationOverflow.svelte
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<script>
|
||||||
|
export let fromIndex = 0;
|
||||||
|
export let count = 0;
|
||||||
|
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import OverflowMenuHorizontal16 from "carbon-icons-svelte/lib/OverflowMenuHorizontal16";
|
||||||
|
import PaginationItem from "./PaginationItem.svelte";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let value = "";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if count > 1}
|
||||||
|
<li class:bx--pagination-nav__list-item={true}>
|
||||||
|
<div class:bx--pagination-nav__select={true}>
|
||||||
|
<!-- svelte-ignore a11y-no-onchange -->
|
||||||
|
<select
|
||||||
|
aria-label="Select Page number"
|
||||||
|
{value}
|
||||||
|
class:bx--pagination-nav__page={true}
|
||||||
|
class:bx--pagination-nav__page--select={true}
|
||||||
|
on:change={({ target }) => {
|
||||||
|
value = '';
|
||||||
|
dispatch('select', { index: Number(target.value) });
|
||||||
|
}}>
|
||||||
|
<option value="" hidden />
|
||||||
|
{#each Array.from({ length: count }, (_, i) => i) as i}
|
||||||
|
<option value={fromIndex + i} data-page={fromIndex + i + 1}>
|
||||||
|
{fromIndex + i + 1}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<div class:bx--pagination-nav__select-icon-wrapper={true}>
|
||||||
|
<OverflowMenuHorizontal16 class="bx--pagination-nav__select-icon" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{:else if count === 1}
|
||||||
|
<PaginationItem
|
||||||
|
page={fromIndex + 1}
|
||||||
|
on:click={() => {
|
||||||
|
dispatch('select', { index: fromIndex });
|
||||||
|
}}>
|
||||||
|
Page
|
||||||
|
</PaginationItem>
|
||||||
|
{/if}
|
1
src/PaginationNav/index.js
Normal file
1
src/PaginationNav/index.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { default as PaginationNav } from "./PaginationNav.svelte";
|
|
@ -65,6 +65,7 @@ export { NumberInput, NumberInputSkeleton } from "./NumberInput";
|
||||||
export { OrderedList } from "./OrderedList";
|
export { OrderedList } from "./OrderedList";
|
||||||
export { OverflowMenu, OverflowMenuItem } from "./OverflowMenu";
|
export { OverflowMenu, OverflowMenuItem } from "./OverflowMenu";
|
||||||
export { Pagination, PaginationSkeleton } from "./Pagination";
|
export { Pagination, PaginationSkeleton } from "./Pagination";
|
||||||
|
export { PaginationNav } from "./PaginationNav";
|
||||||
export {
|
export {
|
||||||
ProgressIndicator,
|
ProgressIndicator,
|
||||||
ProgressIndicatorSkeleton,
|
ProgressIndicatorSkeleton,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue