diff --git a/package-lock.json b/package-lock.json index 1687430..5ec07cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4776,6 +4776,10 @@ "resolved": "packages/navigation-drawer", "link": true }, + "node_modules/@pepabo-inhouse/pagination": { + "resolved": "packages/pagination", + "link": true + }, "node_modules/@pepabo-inhouse/progress-indicator": { "resolved": "packages/progress-indicator", "link": true @@ -31250,6 +31254,7 @@ "@pepabo-inhouse/interactive-table": "^4.0.0", "@pepabo-inhouse/list": "^4.0.0", "@pepabo-inhouse/navigation-drawer": "^4.0.0", + "@pepabo-inhouse/pagination": "^4.0.0", "@pepabo-inhouse/progress-indicator": "^4.0.0", "@pepabo-inhouse/radio": "^4.0.0", "@pepabo-inhouse/scrim": "^4.0.0", @@ -31431,6 +31436,17 @@ "@pepabo-inhouse/tokens": "^2.4.0" } }, + "packages/pagination": { + "version": "4.0.0", + "dependencies": { + "@pepabo-inhouse/adapter": "^4.0.0", + "@pepabo-inhouse/icon": "^4.0.0" + }, + "devDependencies": { + "@pepabo-inhouse/flavor": "^4.0.0", + "@pepabo-inhouse/tokens": "^2.4.0" + } + }, "packages/progress-indicator": { "name": "@pepabo-inhouse/progress-indicator", "version": "4.0.0", diff --git a/packages/components-web/inhouse-components-web.scss b/packages/components-web/inhouse-components-web.scss index 583e81e..016bdf4 100644 --- a/packages/components-web/inhouse-components-web.scss +++ b/packages/components-web/inhouse-components-web.scss @@ -19,6 +19,7 @@ @use "@pepabo-inhouse/interactive-table"; @use "@pepabo-inhouse/list"; @use "@pepabo-inhouse/navigation-drawer"; +@use "@pepabo-inhouse/pagination"; @use "@pepabo-inhouse/progress-indicator"; @use "@pepabo-inhouse/radio"; @use "@pepabo-inhouse/scrim"; @@ -54,6 +55,7 @@ @include interactive-table.export; @include list.export; @include navigation-drawer.export; +@include pagination.export; @include progress-indicator.export; @include radio.export; @include scrim.export; diff --git a/packages/components-web/package.json b/packages/components-web/package.json index 4aef1db..8b0e790 100644 --- a/packages/components-web/package.json +++ b/packages/components-web/package.json @@ -34,6 +34,7 @@ "@pepabo-inhouse/interactive-table": "^4.0.0", "@pepabo-inhouse/list": "^4.0.0", "@pepabo-inhouse/navigation-drawer": "^4.0.0", + "@pepabo-inhouse/pagination": "^4.0.0", "@pepabo-inhouse/progress-indicator": "^4.0.0", "@pepabo-inhouse/radio": "^4.0.0", "@pepabo-inhouse/scrim": "^4.0.0", diff --git a/packages/pagination/README.md b/packages/pagination/README.md new file mode 100644 index 0000000..dc3ba68 --- /dev/null +++ b/packages/pagination/README.md @@ -0,0 +1,13 @@ +# Inhouse Pagination + +## Usage + +### Installation + +```bash +$ npm install @pepabo-inhouse/pagination + +# or + +$ yarn add @pepabo-inhouse/pagination +``` diff --git a/packages/pagination/_index.scss b/packages/pagination/_index.scss new file mode 100644 index 0000000..9e8069c --- /dev/null +++ b/packages/pagination/_index.scss @@ -0,0 +1 @@ +@forward "./mixins" show style, export; diff --git a/packages/pagination/_mixins.scss b/packages/pagination/_mixins.scss new file mode 100644 index 0000000..17d67d1 --- /dev/null +++ b/packages/pagination/_mixins.scss @@ -0,0 +1,118 @@ +@use "sass:map"; +@use "@pepabo-inhouse/adapter/functions" as adapter; +@use "@pepabo-inhouse/icon/mixins" as icon; +@use "./variables"; + +@mixin style($options: variables.$default-option) { + $options: map.merge(variables.$default-option, $options); + + display: flex; + gap: adapter.get-spacing-size($level: xs); + align-items: center; + justify-content: center; + + ._list { + display: flex; + flex-wrap: wrap; + gap: adapter.get-spacing-size($level: xs); + justify-content: center; + margin: 0; + padding: 0; + list-style: none; + } + + ._item { + display: inline-flex; + } + + @include -size-rule($size: map.get($options, size)); + + ._link, + ._previous, + ._next { + color: adapter.get-implication-color($name: interactive, $level: 700); + text-decoration: underline; + + &:hover { + background: adapter.get-background-overlay-color($brightness: light, $state: hover); + } + + &:active { + background: adapter.get-background-overlay-color($brightness: light, $state: active); + } + + &:focus-visible { + outline: adapter.get-focus-ring-outline(); + outline-offset: adapter.get-focus-ring-outline-offset(); + } + } + + ._current { + color: adapter.get-text-color($brightness: light, $name: high_emphasis); + font-weight: bold; + background: adapter.get-surface-color(light, secondary); + } + + ._truncation { + color: adapter.get-text-color($brightness: light, $name: low_emphasis); + } + + ._counter { + color: adapter.get-text-color($brightness: light, $name: high_emphasis); + } + + ._previous::before { + @include icon.style; + + content: "chevron_left"; + } + + ._next::after { + @include icon.style; + + content: "chevron_right"; + } + + // 端のページでは Previous / Next を出力しない代わりにプレースホルダーを置き、 + // 要素分の領域を保持する(レイアウトシフトと連続操作中の誤クリックを防ぐ) + ._previous.-is-placeholder, + ._next.-is-placeholder { + visibility: hidden; + } + + @each $size in adapter.get-interactive-component-height-keys() { + &.-size-#{$size} { + @include -size-rule($size: $size); + } + } +} + +@mixin export { + .in-pagination { + @include style; + } +} + +@mixin -size-rule($size) { + ._link, + ._current, + ._truncation, + ._previous, + ._next { + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + min-width: adapter.get-interactive-component-height($level: $size); + height: adapter.get-interactive-component-height($level: $size); + padding-inline: adapter.get-spacing-size($level: xs); + font-size: adapter.get-font-size($level: $size); + line-height: adapter.get-line-height($level: $size, $density: normal); + border-radius: adapter.get-radius-size($level: m); + } + + ._counter { + font-size: adapter.get-font-size($level: $size); + line-height: adapter.get-line-height($level: $size, $density: normal); + } +} diff --git a/packages/pagination/_variables.scss b/packages/pagination/_variables.scss new file mode 100644 index 0000000..5d17522 --- /dev/null +++ b/packages/pagination/_variables.scss @@ -0,0 +1,3 @@ +$default-option: ( + size: m, +); diff --git a/packages/pagination/inhouse-pagination.scss b/packages/pagination/inhouse-pagination.scss new file mode 100644 index 0000000..b2be6e9 --- /dev/null +++ b/packages/pagination/inhouse-pagination.scss @@ -0,0 +1,2 @@ +@use "./mixins"; +@include mixins.export; diff --git a/packages/pagination/package.json b/packages/pagination/package.json new file mode 100644 index 0000000..b2df797 --- /dev/null +++ b/packages/pagination/package.json @@ -0,0 +1,21 @@ +{ + "name": "@pepabo-inhouse/pagination", + "description": "Inhouse Components for the web pagination component", + "version": "4.0.0", + "repository": { + "type": "git", + "url": "https://github.com/pepabo/inhouse-components-web.git", + "directory": "packages/pagination" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pepabo-inhouse/adapter": "^4.0.0", + "@pepabo-inhouse/icon": "^4.0.0" + }, + "devDependencies": { + "@pepabo-inhouse/flavor": "^4.0.0", + "@pepabo-inhouse/tokens": "^2.4.0" + } +} diff --git a/packages/stories-web/src/components/Pagination.tsx b/packages/stories-web/src/components/Pagination.tsx new file mode 100644 index 0000000..29f27ff --- /dev/null +++ b/packages/stories-web/src/components/Pagination.tsx @@ -0,0 +1,111 @@ +import React, { FC, HTMLAttributes } from 'react' +import { Size } from './types' + +type HTMLProps = HTMLAttributes + +export type Format = 'numbered' | 'simple' + +export interface Props extends HTMLProps { + current: number; + max: number; + format?: Format; + siblingCount?: number; + withCounter?: boolean; + size?: Extract; + ariaLabel?: string; + hrefTemplate?: (page: number) => string; +} + +type PageItem = number | 'truncation' + +const getPageItems = (current: number, max: number, siblingCount: number): PageItem[] => { + const first = 1; + const last = max; + const start = Math.max(current - siblingCount, first + 1); + const end = Math.min(current + siblingCount, last - 1); + + const items: PageItem[] = [first]; + + if (start > first + 1) { + items.push('truncation'); + } + + for (let page = start; page <= end; page++) { + items.push(page); + } + + if (end < last - 1) { + items.push('truncation'); + } + + if (last > first) { + items.push(last); + } + + return items; +}; + +const Pagination: FC = (props: Props) => { + const { + current, + max, + format = 'numbered', + siblingCount = 2, + withCounter = false, + size, + ariaLabel, + hrefTemplate = (page: number) => `?page=${page}`, + ...rest + } = props; + + const classList = ['in-pagination']; + + if (typeof size !== 'undefined') { + classList.push(`-size-${size}`); + } + + const classes = classList.join(' '); + + return ( + + ); +}; + +export default Pagination; diff --git a/packages/stories-web/src/pagination.stories.tsx b/packages/stories-web/src/pagination.stories.tsx new file mode 100644 index 0000000..bd4af8e --- /dev/null +++ b/packages/stories-web/src/pagination.stories.tsx @@ -0,0 +1,51 @@ +import type { StoryFn, Meta } from '@storybook/react' +import React from 'react' +import Pagination, { Props } from './components/Pagination' + +export default { + title: 'Components/Pagination', + component: Pagination, + argTypes: { + hrefTemplate: { table: { disable: true } } + } +} as Meta + +const Template: StoryFn = (args) => { + return ( + + ) +} + +export const Index = Template.bind({}) +Index.args = { + current: 5, + max: 10 +} + +export const FirstPage = Template.bind({}) +FirstPage.args = { + current: 1, + max: 10 +} + +export const LastPage = Template.bind({}) +LastPage.args = { + current: 10, + max: 10 +} + +export const FewPages = Template.bind({}) +FewPages.args = { + current: 2, + max: 3 +} + +export const Simple = Template.bind({}) +Simple.args = { + current: 5, + max: 10, + format: 'simple' +}