diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index a82e0689..dfe3d84c 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v2 with: - ref: ${{ github.event.pull_request.head.ref }} + ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - uses: nrwl/nx-set-shas@v2 - uses: actions/setup-node@v2 @@ -25,7 +25,7 @@ jobs: steps: - uses: actions/checkout@v2 with: - ref: ${{ github.event.pull_request.head.ref }} + ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - uses: nrwl/nx-set-shas@v2 - uses: actions/setup-node@v2 @@ -40,7 +40,7 @@ jobs: steps: - uses: actions/checkout@v2 with: - ref: ${{ github.event.pull_request.head.ref }} + ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - uses: nrwl/nx-set-shas@v2 - uses: actions/setup-node@v2 diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 5a1c63c2..4a4e2abb 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -31,6 +31,9 @@ export { DInput, DInputAffix } from './input'; export type { DMenuProps, DMenuGroupProps, DMenuItemProps, DMenuSubProps } from './menu'; export { DMenu, DMenuGroup, DMenuItem, DMenuSub } from './menu'; +export type { DPaginationProps } from './pagination'; +export { DPagination } from './pagination'; + export type { DRadioProps, DRadioGroupProps } from './radio'; export { DRadio, DRadioGroup } from './radio'; diff --git a/packages/ui/src/components/pagination/Pagination.tsx b/packages/ui/src/components/pagination/Pagination.tsx new file mode 100644 index 00000000..4b75f5ee --- /dev/null +++ b/packages/ui/src/components/pagination/Pagination.tsx @@ -0,0 +1,410 @@ +import type { Updater } from '../../hooks/two-way-binding'; + +import React, { useCallback, useMemo, useState } from 'react'; + +import { usePrefixConfig, useComponentConfig, useTwoWayBinding, useTranslation, useAsync } from '../../hooks'; +import { getClassName } from '../../utils'; +import { DIcon } from '../icon'; +import { DInput, DInputAffix } from '../input'; +import { DSelect } from '../select'; + +export interface DPaginationProps extends React.HTMLAttributes { + dActive?: [number, Updater?]; + dTotal: number; + dPageSize?: [number, Updater?]; + dPageSizeOptions?: number[]; + dCompose?: Array<'total' | 'pages' | 'size' | 'jump'>; + dCustomRender?: { + total?: (range: [number, number]) => React.ReactNode; + prev?: React.ReactNode; + page?: (page: number) => React.ReactNode; + next?: React.ReactNode; + sizeOption?: (size: number) => React.ReactNode; + jump?: (input: React.ReactNode) => React.ReactNode; + }; + dMini?: boolean; + dDisabled?: boolean; + onActiveChange?: (page: number) => void; + onPageSizeChange?: (size: number) => void; +} + +const DEFAULT_PROPS = { + dCompose: ['pages'], + dPageSizeOptions: [10, 20, 50, 100], +}; +export function DPagination(props: DPaginationProps) { + const { + dActive, + dTotal, + dPageSize, + dPageSizeOptions = DEFAULT_PROPS.dPageSizeOptions, + dCompose = DEFAULT_PROPS.dCompose, + dCustomRender, + dMini = false, + dDisabled = false, + onActiveChange, + onPageSizeChange, + className, + children, + ...restProps + } = useComponentConfig(DPagination.name, props); + + //#region Context + const dPrefix = usePrefixConfig(); + //#endregion + + const asyncCapture = useAsync(); + const [t] = useTranslation('DPagination'); + + const [isChange, setIsChange] = useState(false); + const [jumpValue, setJumpValue] = useState(''); + + const [active, _changeActive] = useTwoWayBinding(1, dActive, onActiveChange); + const [pageSize, changePageSize] = useTwoWayBinding(10, dPageSize, onPageSizeChange); + + const changeActive = useCallback( + (active: number) => { + _changeActive(active); + + setIsChange(true); + asyncCapture.requestAnimationFrame(() => asyncCapture.setTimeout(() => setIsChange(false))); + }, + [_changeActive, asyncCapture] + ); + + const lastPage = Math.max(Math.ceil(dTotal / pageSize), 1); + const iconSize = 14; + + if (lastPage < active) { + _changeActive(lastPage); + } + + const totalNode = useMemo(() => { + if (dCompose.includes('total')) { + const range: [number, number] = [Math.min((active - 1) * pageSize + 1, dTotal), Math.min(active * pageSize, dTotal)]; + if (dCustomRender && dCustomRender.total) { + return dCustomRender.total(range); + } else { + return ( + + {t('Total')} {dTotal} {t('items')} + + ); + } + } + return null; + }, [active, dCompose, dCustomRender, dTotal, pageSize, t]); + + const [prevNode, pageNode, nextNode] = useMemo(() => { + let [prevNode, nextNode]: [React.ReactNode, React.ReactNode] = [null, null]; + if (dCompose.includes('pages')) { + if (dCustomRender && dCustomRender.prev) { + prevNode = dCustomRender.prev; + } else { + prevNode = ( + + + + ); + } + prevNode = ( +
  • { + changeActive(Math.max(active - 1, 1)); + }} + onKeyDown={(e) => { + if (e.code === 'Enter' || e.code === 'Space') { + e.preventDefault(); + changeActive(Math.max(active - 1, 1)); + } + }} + > + {prevNode} +
  • + ); + + if (dCustomRender && dCustomRender.next) { + nextNode = dCustomRender.next; + } else { + nextNode = ( + + {' '} + + ); + } + nextNode = ( +
  • { + changeActive(Math.min(active + 1, lastPage)); + }} + onKeyDown={(e) => { + if (e.code === 'Enter' || e.code === 'Space') { + e.preventDefault(); + changeActive(Math.min(active + 1, lastPage)); + } + }} + > + {nextNode} +
  • + ); + } + return [ + prevNode, + (page: number) => { + if (dCustomRender && dCustomRender.page) { + return dCustomRender.page(page); + } else { + return {page}; + } + }, + nextNode, + ]; + }, [active, changeActive, dCompose, dCustomRender, dDisabled, dPrefix, lastPage, t]); + + const sizeNode = useMemo(() => { + const options = dPageSizeOptions.map((size) => ({ + dLabel: size.toString(), + dValue: size, + })); + + return ( + `${select.dLabel} ${t(' / Page')}`} + dOptionRender={(option) => (dCustomRender && dCustomRender.sizeOption ? dCustomRender.sizeOption(option.dValue) : option.dLabel)} + dDisabled={dDisabled} + onSelectChange={(select) => { + changePageSize(select as number); + }} + > + ); + }, [changePageSize, dCustomRender, dDisabled, dMini, dPageSizeOptions, pageSize, t]); + + const jumpNode = useMemo(() => { + if (dCompose.includes('jump')) { + const inputNode = ( + { + if (e.code === 'Enter') { + e.preventDefault(); + const value = Number(jumpValue); + if (!Number.isNaN(value)) { + changeActive(Math.max(Math.min(value, lastPage), 1)); + } + setJumpValue(''); + } + }} + /> + ); + const jumpInput = dMini ? ( + inputNode + ) : ( + + {inputNode} + + ); + + if (dCustomRender && dCustomRender.jump) { + return dCustomRender.jump(jumpInput); + } else { + return ( +
    + {t('Go')} {jumpInput} {t('Page')} +
    + ); + } + } + return null; + }, [changeActive, dCompose, dCustomRender, dDisabled, dMini, jumpValue, lastPage, t]); + + return ( + + ); +} diff --git a/packages/ui/src/components/pagination/README.md b/packages/ui/src/components/pagination/README.md new file mode 100644 index 00000000..65651e3c --- /dev/null +++ b/packages/ui/src/components/pagination/README.md @@ -0,0 +1,31 @@ +--- +group: Navigation +title: Pagination +--- + +Divide a series of pages into different pages. + +## When To Use + +Often used for page navigation or table paging. + +## API + +### DPaginationProps + +Extend `React.HTMLAttributes`. + + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| dActive | Manually control the number of active pages | [number, Updater\?] | 1 | +| dTotal | Total number of entries | number | - | +| dPageSize | Number of entries per page | [number, Updater\?] | 10 | +| dPageSizeOptions | Number of items per page to choose from | number[] | [10, 20, 50, 100] | +| dCompose | Free combination configuration | `Array<'total' \| 'pages' \| 'size' \| 'jump'>` | ['pages'] | +| dCustomRender | Custom configuration | `{ total?: (range: [number, number]) => React.ReactNode; prev?: React.ReactNode; page?: (page: number) => React.ReactNode; next?: React.ReactNode; sizeOption?: (size: number) => React.ReactNode; jump?: (input: React.ReactNode) => React.ReactNode; }` | - | +| dMini | Mini form | boolean | false | +| dDisabled | Whether to disable | boolean | false | +| onActiveChange | Callback when the number of active pages has changed | `(page: number) => void` | - | +| onPageSizeChange | Callback when the number of items per page has changed | `(size: number) => void` | - | + diff --git a/packages/ui/src/components/pagination/README.zh-Hant.md b/packages/ui/src/components/pagination/README.zh-Hant.md new file mode 100644 index 00000000..62f273ec --- /dev/null +++ b/packages/ui/src/components/pagination/README.zh-Hant.md @@ -0,0 +1,30 @@ +--- +title: 分页 +--- + +将一系列页面分割到不同页。 + +## 何时使用 + +常用于页面导航或者表格分页。 + +## API + +### DPaginationProps + +继承 `React.HTMLAttributes`。 + + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| dActive | 手动控制活动的页数 | [number, Updater\?] | 1 | +| dTotal | 条目总数 | number | - | +| dPageSize | 每页包含条目数 | [number, Updater\?] | 10 | +| dPageSizeOptions | 可供选择的每页条目数 | number[] | [10, 20, 50, 100] | +| dCompose | 自由组合配置 | `Array<'total' \| 'pages' \| 'size' \| 'jump'>` | ['pages'] | +| dCustomRender | 自定义配置 | `{ total?: (range: [number, number]) => React.ReactNode; prev?: React.ReactNode; page?: (page: number) => React.ReactNode; next?: React.ReactNode; sizeOption?: (size: number) => React.ReactNode; jump?: (input: React.ReactNode) => React.ReactNode; }` | - | +| dMini | 迷你形态 | boolean | false | +| dDisabled | 是否禁用 | boolean | false | +| onActiveChange | 活动页数改变的回调 | `(page: number) => void` | - | +| onPageSizeChange | 每页条目数改变的回调 | `(size: number) => void` | - | + diff --git a/packages/ui/src/components/pagination/demos/1.Basic.md b/packages/ui/src/components/pagination/demos/1.Basic.md new file mode 100644 index 00000000..93fc9e72 --- /dev/null +++ b/packages/ui/src/components/pagination/demos/1.Basic.md @@ -0,0 +1,21 @@ +--- +title: + en-US: Basic + zh-Hant: 基本 +--- + +# en-US + +The simplest usage. + +# zh-Hant + +最简单的用法。 + +```tsx +import { DPagination } from '@react-devui/ui'; + +export default function Demo() { + return ; +} +``` diff --git a/packages/ui/src/components/pagination/demos/2.PageSizeOptions.md b/packages/ui/src/components/pagination/demos/2.PageSizeOptions.md new file mode 100644 index 00000000..c947951b --- /dev/null +++ b/packages/ui/src/components/pagination/demos/2.PageSizeOptions.md @@ -0,0 +1,21 @@ +--- +title: + en-US: Page size + zh-Hant: 每页大小 +--- + +# en-US + +To switch the size of each page, you can customize the options through `dPageSizeOptions`. + +# zh-Hant + +切换每页大小,可通过 `dPageSizeOptions` 自定义可选项。 + +```tsx +import { DPagination } from '@react-devui/ui'; + +export default function Demo() { + return ; +} +``` diff --git a/packages/ui/src/components/pagination/demos/3.All.md b/packages/ui/src/components/pagination/demos/3.All.md new file mode 100644 index 00000000..fd2a76c8 --- /dev/null +++ b/packages/ui/src/components/pagination/demos/3.All.md @@ -0,0 +1,21 @@ +--- +title: + en-US: All configurations + zh-Hant: 所有配置 +--- + +# en-US + +Configure freely through `dCompose`. + +# zh-Hant + +通过 `dCompose` 自由组合配置。 + +```tsx +import { DPagination } from '@react-devui/ui'; + +export default function Demo() { + return ; +} +``` diff --git a/packages/ui/src/components/pagination/demos/4.Mini.md b/packages/ui/src/components/pagination/demos/4.Mini.md new file mode 100644 index 00000000..826facee --- /dev/null +++ b/packages/ui/src/components/pagination/demos/4.Mini.md @@ -0,0 +1,21 @@ +--- +title: + en-US: Mini + zh-Hant: 迷你 +--- + +# en-US + +Mini form. + +# zh-Hant + +迷你形态。 + +```tsx +import { DPagination } from '@react-devui/ui'; + +export default function Demo() { + return ; +} +``` diff --git a/packages/ui/src/components/pagination/demos/5.Customize.md b/packages/ui/src/components/pagination/demos/5.Customize.md new file mode 100644 index 00000000..246ca394 --- /dev/null +++ b/packages/ui/src/components/pagination/demos/5.Customize.md @@ -0,0 +1,38 @@ +--- +title: + en-US: Customize + zh-Hant: 自定义 +--- + +# en-US + +All configurations can be customized through `dCustomRender`. + +# zh-Hant + +通过 `dCustomRender` 可自定义所有配置。 + +```tsx +import { DPagination } from '@react-devui/ui'; + +export default function Demo() { + return ( + `${range[0]}-${range[1]} of 200`, + prev: 'prev', + page: (page) => {`P${page}`}, + next: 'next', + sizeOption: (size) => `size: ${size}`, + jump: (input) => ( + <> + To {input} + + ), + }} + > + ); +} +``` diff --git a/packages/ui/src/components/pagination/demos/6.Disabled.md b/packages/ui/src/components/pagination/demos/6.Disabled.md new file mode 100644 index 00000000..8ff781df --- /dev/null +++ b/packages/ui/src/components/pagination/demos/6.Disabled.md @@ -0,0 +1,27 @@ +--- +title: + en-US: Disable + zh-Hant: 禁用 +--- + +# en-US + +Disable via `dDisabled`. + +# zh-Hant + +通过 `dDisabled` 禁用。 + +```tsx +import { DPagination } from '@react-devui/ui'; + +export default function Demo() { + return ( + <> + +
    + + + ); +} +``` diff --git a/packages/ui/src/components/pagination/index.ts b/packages/ui/src/components/pagination/index.ts new file mode 100644 index 00000000..e016c96b --- /dev/null +++ b/packages/ui/src/components/pagination/index.ts @@ -0,0 +1 @@ +export * from './Pagination'; diff --git a/packages/ui/src/hooks/i18n/resources.json b/packages/ui/src/hooks/i18n/resources.json index e1846928..c9ea8306 100644 --- a/packages/ui/src/hooks/i18n/resources.json +++ b/packages/ui/src/hooks/i18n/resources.json @@ -52,5 +52,43 @@ "en-US": "Decrease number", "zh-Hant": "数字减" } + }, + "DPagination": { + "Total": { + "en-US": "Total", + "zh-Hant": "共" + }, + "items": { + "en-US": "items", + "zh-Hant": "项" + }, + "Go": { + "en-US": "Go", + "zh-Hant": "至" + }, + "Page": { + "en-US": "Page", + "zh-Hant": "页" + }, + "5 pages forward": { + "en-US": "5 pages forward", + "zh-Hant": "向前 5 页" + }, + "5 pages backward": { + "en-US": "5 pages backward", + "zh-Hant": "向后 5 页" + }, + "Previous page": { + "en-US": "Previous page", + "zh-Hant": "上一页" + }, + "Next page": { + "en-US": "Next page", + "zh-Hant": "下一页" + }, + " / Page": { + "en-US": " / Page", + "zh-Hant": "条/页" + } } } diff --git a/packages/ui/src/styles/_components.scss b/packages/ui/src/styles/_components.scss index 6467b0e2..4c360566 100644 --- a/packages/ui/src/styles/_components.scss +++ b/packages/ui/src/styles/_components.scss @@ -12,6 +12,7 @@ @import 'components/icon'; @import 'components/input'; @import 'components/menu'; +@import 'components/pagination'; @import 'components/popup'; @import 'components/radio'; @import 'components/select-box'; diff --git a/packages/ui/src/styles/components/_button.scss b/packages/ui/src/styles/components/_button.scss index c293ff41..03e5ec9e 100644 --- a/packages/ui/src/styles/components/_button.scss +++ b/packages/ui/src/styles/components/_button.scss @@ -31,7 +31,6 @@ font-size: var(--#{$variable-prefix}font); font-family: inherit; - line-height: inherit; white-space: nowrap; text-align: unset; text-transform: none; diff --git a/packages/ui/src/styles/components/_dropdown.scss b/packages/ui/src/styles/components/_dropdown.scss index 689bb650..5f8f4b4d 100644 --- a/packages/ui/src/styles/components/_dropdown.scss +++ b/packages/ui/src/styles/components/_dropdown.scss @@ -1,9 +1,11 @@ +$dropdown-item-height: 32px; + @mixin dropdown-item() { position: relative; display: flex; align-items: center; - height: 32px; + height: $dropdown-item-height; margin: 0; list-style: none; @@ -41,7 +43,7 @@ display: flex; align-items: center; width: 100%; - height: 32px; + height: $dropdown-item-height; padding: 0 12px; color: var(--#{$variable-prefix}color-step-400); @@ -49,12 +51,10 @@ } @include b(dropdown) { - --#{$variable-prefix}dropdown-item-height: 32px; - position: relative; min-width: 120px; - max-height: calc(8px + 32px * 6); + max-height: calc(8px + #{$dropdown-item-height} * 6); padding: 4px 0; overflow-x: hidden; @@ -112,7 +112,7 @@ display: flex; align-items: center; - height: 32px; + height: $dropdown-item-height; margin: 0; padding: 0 12px; diff --git a/packages/ui/src/styles/components/_header.scss b/packages/ui/src/styles/components/_header.scss index 4792d8aa..f08c0396 100644 --- a/packages/ui/src/styles/components/_header.scss +++ b/packages/ui/src/styles/components/_header.scss @@ -11,7 +11,6 @@ align-items: center; font-weight: 500; - line-height: 22px; @include font-size(1.15rem); } diff --git a/packages/ui/src/styles/components/_pagination.scss b/packages/ui/src/styles/components/_pagination.scss new file mode 100644 index 00000000..83f11ca7 --- /dev/null +++ b/packages/ui/src/styles/components/_pagination.scss @@ -0,0 +1,147 @@ +@include b(pagination) { + --#{$variable-prefix}pagination-item-size: 32px; + display: flex; + flex-wrap: wrap; + gap: 8px 16px; + align-items: center; + margin: 0; + padding: 0; + + outline: none; + + @include font-size(0.95rem); + + &:not(.is-disabled) { + .#{$variable-prefix}pagination__item--button:not(.is-disabled) { + &:hover, + &:focus { + border-color: var(--#{$variable-prefix}color-primary-lighter); + + color: var(--#{$variable-prefix}color-primary-lighter); + } + + @include when(active) { + border-color: var(--#{$variable-prefix}color-primary); + + color: var(--#{$variable-prefix}color-primary); + } + } + } + + @include when(disabled) { + cursor: not-allowed; + + .#{$variable-prefix}pagination__item--button { + color: var(--#{$variable-prefix}disabled-color); + + background-color: var(--#{$variable-prefix}disabled-background-color); + + pointer-events: none; + + @include when(active) { + color: rgb(var(--#{$variable-prefix}color-primary-rgb) / 50%); + + background-color: var(--#{$variable-prefix}color-step-100); + } + } + } + + @include when(change) { + .#{$variable-prefix}pagination__item--button { + transition: none; + } + } + + @include m(mini) { + --#{$variable-prefix}pagination-item-size: 24px; + + gap: 4px 8px; + + @include font-size(0.8rem); + + .#{$variable-prefix}pagination__list { + gap: 4px 2px; + } + .#{$variable-prefix}pagination__item--button:not(.is-active) { + border: none; + } + } + + @include e(list) { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 0; + padding: 0; + + list-style: none; + } + + @include e(item) { + position: relative; + + display: inline-flex; + align-items: center; + + vertical-align: top; + + @include m(button) { + justify-content: center; + min-width: var(--#{$variable-prefix}pagination-item-size); + height: var(--#{$variable-prefix}pagination-item-size); + border-radius: var(--#{$variable-prefix}border-radius); + + outline: none; + cursor: pointer; + + transition: border-color 0.2s linear, color 0.2s linear; + + user-select: none; + + @include utils-disabled; + } + + @include m(border) { + border: 1px solid var(--#{$variable-prefix}border-color); + } + + @include m(jump5) { + .#{$variable-prefix}icon { + color: var(--#{$variable-prefix}color-primary); + + opacity: 0; + + transition: opacity 133ms linear; + } + + &:hover, + &:focus { + .#{$variable-prefix}icon { + opacity: 1; + } + .#{$variable-prefix}pagination__ellipsis { + opacity: 0; + } + } + } + } + + @include e(ellipsis) { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + + display: inline-flex; + align-items: center; + justify-content: center; + + color: var(--#{$variable-prefix}color-step-200); + font-size: 0.85em; + + letter-spacing: 1.5px; + + transition: opacity 133ms linear; + } +} diff --git a/packages/ui/src/styles/components/_select-box.scss b/packages/ui/src/styles/components/_select-box.scss index b4aabb3d..31fdd748 100644 --- a/packages/ui/src/styles/components/_select-box.scss +++ b/packages/ui/src/styles/components/_select-box.scss @@ -12,7 +12,6 @@ font-size: var(--#{$variable-prefix}font); font-family: inherit; - line-height: inherit; white-space: nowrap; text-align: unset; text-transform: none; diff --git a/packages/ui/src/styles/components/_select.scss b/packages/ui/src/styles/components/_select.scss index 169dcaf2..2875af5a 100644 --- a/packages/ui/src/styles/components/_select.scss +++ b/packages/ui/src/styles/components/_select.scss @@ -1,3 +1,5 @@ +$select-option-height: 32px; + @include b(select) { @include m(multiple) { .#{$variable-prefix}select-box__content { @@ -42,7 +44,7 @@ display: flex; align-items: center; width: 100%; - height: 32px; + height: $select-option-height; padding: 0 12px; outline: none; diff --git a/packages/ui/src/styles/components/_tabs.scss b/packages/ui/src/styles/components/_tabs.scss index 60667c3d..65e18823 100644 --- a/packages/ui/src/styles/components/_tabs.scss +++ b/packages/ui/src/styles/components/_tabs.scss @@ -462,8 +462,6 @@ display: flex; height: 100%; - line-height: 22px; - @include e(close) { margin-left: auto; }