From 40d9debab63009bd8bdf02d4a856811aa6459f7a Mon Sep 17 00:00:00 2001 From: Jen Downs Date: Tue, 2 Jun 2020 11:57:31 -0500 Subject: [PATCH] feat(unstable-pagination): add refactored experimental pagination component (#5485) * feat(unstable_pagination): initial commit of js, scss, and story demo * fix(unstable-pagination): update naming structure * fix(unstable-pagination): add styles for refactored component * fix(unstable-pagination): update imports * fix(unstable-pagination): flatten styles * fix(unstable-pagination): update class names * fix(unstable-pagination): update comments and formatting * chore: run prettier * chore: update exports snapshot * feat(unstable-pagination): remove page-input subcomponent * fix(pagination): remove page-input styles * fix: remove page input from api snapshot * fix(unstable-pagination): update left and right section height * test(api): update public api snapshot to include new components * fix(unstable-pagination): adjust btn focus outlines * fix(unstable-pagination): update select hover state, update borders Co-authored-by: TJ Egan --- .../pagination/_unstable_pagination.scss | 177 +++++++++++ .../components/src/globals/scss/styles.scss | 1 + packages/react/.storybook/styles.scss | 1 + .../__snapshots__/PublicAPI-test.js.snap | 131 +++++++++ packages/react/src/__tests__/index-test.js | 2 + .../Unstable_Pagination/PageSelector.js | 80 +++++ .../Unstable_Pagination/Pagination-story.js | 109 +++++++ .../Unstable_Pagination/Pagination.js | 277 ++++++++++++++++++ .../Pagination/Unstable_Pagination/index.js | 9 + packages/react/src/index.js | 6 + 10 files changed, 793 insertions(+) create mode 100644 packages/components/src/components/pagination/_unstable_pagination.scss create mode 100644 packages/react/src/components/Pagination/Unstable_Pagination/PageSelector.js create mode 100644 packages/react/src/components/Pagination/Unstable_Pagination/Pagination-story.js create mode 100644 packages/react/src/components/Pagination/Unstable_Pagination/Pagination.js create mode 100644 packages/react/src/components/Pagination/Unstable_Pagination/index.js diff --git a/packages/components/src/components/pagination/_unstable_pagination.scss b/packages/components/src/components/pagination/_unstable_pagination.scss new file mode 100644 index 000000000000..2241450a49b7 --- /dev/null +++ b/packages/components/src/components/pagination/_unstable_pagination.scss @@ -0,0 +1,177 @@ +// +// Copyright IBM Corp. 2016, 2020 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +$css--helpers: true; + +@import '../../globals/scss/vars'; +@import '../../globals/scss/helper-mixins'; +@import '../../globals/scss/typography'; +@import '../../globals/scss/layout'; +@import '../../globals/scss/css--helpers'; +@import '../../globals/scss/vendor/@carbon/elements/scss/import-once/import-once'; +@import '../select/select'; + +/// Unstable pagination styles +/// @access private +/// @group pagination +@mixin unstable_pagination { + .#{$prefix}--unstable-pagination { + @include reset; + @include carbon--type-style('body-short-01'); + width: 100%; + background-color: $ui-01; + display: flex; + align-items: center; + justify-content: space-between; + border-top: 1px solid $ui-03; + border-bottom: 1px solid transparent; + height: rem(48px); + } + + .#{$prefix}--unstable-pagination__text { + @include carbon--breakpoint('md') { + display: inline-block; + } + + margin: 0 $carbon--spacing-05; + color: $text-02; + } + + .#{$prefix}--unstable-pagination__left, + .#{$prefix}--unstable-pagination__right { + display: flex; + height: 100%; + align-items: center; + } + + .#{$prefix}--unstable-pagination__left { + padding: 0 $carbon--spacing-05 0 0; + } + + .#{$prefix}--unstable-pagination__left > .#{$prefix}--form-item, + .#{$prefix}--unstable-pagination__right > .#{$prefix}--form-item { + height: 100%; + } + + .#{$prefix}--unstable-pagination__left + .#{$prefix}--unstable-pagination__text { + margin-right: rem(1px); + } + + .#{$prefix}--unstable-pagination__right + .#{$prefix}--unstable-pagination__text { + margin-right: $carbon--spacing-05; + margin-left: rem(1px); + } + + .#{$prefix}--unstable-pagination__button { + @include reset; + border: none; + border-left: 1px solid $ui-03; + background: none; + cursor: pointer; + height: 100%; + margin: 0; + padding: 0 rem(14px); + display: flex; + justify-content: center; + align-items: center; + color: $ui-05; // for currentColor + fill: $ui-05; + transition: outline $duration--fast-02 motion(standard, productive); + transition: background-color $duration--fast-02 motion(standard, productive); + } + + // Unset height/width set by icon-only button: + .#{$prefix}--unstable-pagination__button .#{$prefix}--btn__icon { + height: unset; + width: unset; + } + + .#{$prefix}--unstable-pagination__button.#{$prefix}--btn--icon-only.#{$prefix}--tooltip__trigger:focus { + @include focus-outline('outline'); + } + + .#{$prefix}--unstable-pagination__button:hover { + background: $hover-ui; + color: $ui-05; + } + + .#{$prefix}--unstable-pagination__button--no-index { + fill: $disabled-02; + cursor: not-allowed; + } + + .#{$prefix}--unstable-pagination__button.#{$prefix}--btn:disabled { + background: transparent; + border-color: $ui-03; + } + + .#{$prefix}--unstable-pagination__button:disabled:hover, + .#{$prefix}--unstable-pagination__button--no-index:hover { + cursor: not-allowed; + fill: $disabled-02; + background: transparent; + } + + .#{$prefix}--unstable-pagination__page-selector, + .#{$prefix}--unstable-pagination__page-sizer { + height: 100%; + align-items: center; + } + + .#{$prefix}--unstable-pagination__page-selector + .#{$prefix}--select-input--inline__wrapper, + .#{$prefix}--unstable-pagination__page-sizer + .#{$prefix}--select-input--inline__wrapper { + display: flex; + height: 100%; + } + + .#{$prefix}--unstable-pagination__page-selector .#{$prefix}--select-input, + .#{$prefix}--unstable-pagination__page-sizer .#{$prefix}--select-input { + @include carbon--type-style('body-short-01'); + width: auto; + min-width: auto; + height: 100%; + padding: 0 $carbon--spacing-08 0 $carbon--spacing-05; + margin-right: -0.65rem; + + @include carbon--breakpoint('md') { + padding-right: carbon--mini-units(4.5); + margin-right: 0; + } + } + + .#{$prefix}--unstable-pagination__page-selector + .#{$prefix}--select-input:hover, + .#{$prefix}--unstable-pagination__page-sizer .#{$prefix}--select-input:hover { + background: $hover-ui; + } + + .#{$prefix}--unstable-pagination__page-selector .#{$prefix}--select__arrow, + .#{$prefix}--unstable-pagination__page-sizer .#{$prefix}--select__arrow { + top: 50%; + transform: translateY(-50%); + + @include carbon--breakpoint('md') { + right: $carbon--spacing-05; + } + } + + .#{$prefix}--unstable-pagination__page-selector { + border-left: 1px solid $ui-03; + } + + .#{$prefix}--unstable-pagination__page-sizer { + border-right: 1px solid $ui-03; + } +} + +@include exports('unstable_pagination') { + @include unstable_pagination; +} diff --git a/packages/components/src/globals/scss/styles.scss b/packages/components/src/globals/scss/styles.scss index 37697269ed6a..bc0c2ddc2eca 100644 --- a/packages/components/src/globals/scss/styles.scss +++ b/packages/components/src/globals/scss/styles.scss @@ -155,6 +155,7 @@ $deprecations--message: 'Deprecated code was found, this code will be removed be //------------------------------------- // 🔬 Experimental //------------------------------------- +@import '../../components/pagination/unstable_pagination'; @import '../../components/ui-shell/ui-shell'; //------------------------------------- diff --git a/packages/react/.storybook/styles.scss b/packages/react/.storybook/styles.scss index 55685d82437e..937addf053cb 100644 --- a/packages/react/.storybook/styles.scss +++ b/packages/react/.storybook/styles.scss @@ -61,6 +61,7 @@ $prefix: 'bx'; @import '~carbon-components/src/components/tabs/tabs'; @import '~carbon-components/src/components/tag/tag'; @import '~carbon-components/src/components/pagination/pagination'; +@import '~carbon-components/src/components/pagination/unstable_pagination'; @import '~carbon-components/src/components/accordion/accordion'; @import '~carbon-components/src/components/progress-indicator/progress-indicator'; @import '~carbon-components/src/components/breadcrumb/breadcrumb'; diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 9a7d45484c44..542e6d1259e4 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -6134,6 +6134,137 @@ Map { }, }, }, + "PageSelector" => Object { + "defaultProps": Object { + "className": null, + "id": 1, + "labelText": "Current page number", + }, + "propTypes": Object { + "className": Object { + "type": "string", + }, + "currentPage": Object { + "isRequired": true, + "type": "number", + }, + "id": Object { + "args": Array [ + Array [ + Object { + "type": "string", + }, + Object { + "type": "number", + }, + ], + ], + "type": "oneOfType", + }, + "labelText": Object { + "type": "string", + }, + "totalPages": Object { + "isRequired": true, + "type": "number", + }, + }, + }, + "Unstable_Pagination" => Object { + "defaultProps": Object { + "backwardText": "Previous page", + "children": undefined, + "className": null, + "disabled": false, + "forwardText": "Next page", + "id": 1, + "initialPage": 1, + "itemRangeText": [Function], + "itemText": [Function], + "itemsPerPageText": "Items per page:", + "pageRangeText": [Function], + "pageSize": 10, + "pageSizes": undefined, + "pageText": [Function], + "pagesUnknown": false, + "totalItems": undefined, + }, + "propTypes": Object { + "backwardText": Object { + "type": "string", + }, + "children": Object { + "args": Array [ + Array [ + Object { + "type": "node", + }, + Object { + "type": "func", + }, + ], + ], + "type": "oneOfType", + }, + "className": Object { + "type": "string", + }, + "disabled": Object { + "type": "bool", + }, + "forwardText": Object { + "type": "string", + }, + "id": Object { + "args": Array [ + Array [ + Object { + "type": "string", + }, + Object { + "type": "number", + }, + ], + ], + "type": "oneOfType", + }, + "initialPage": Object { + "type": "number", + }, + "itemRangeText": Object { + "type": "func", + }, + "itemText": Object { + "type": "func", + }, + "itemsPerPageText": Object { + "type": "string", + }, + "pageRangeText": Object { + "type": "func", + }, + "pageSize": Object { + "type": "number", + }, + "pageSizes": Object { + "args": Array [ + Object { + "type": "number", + }, + ], + "type": "arrayOf", + }, + "pageText": Object { + "type": "func", + }, + "pagesUnknown": Object { + "type": "bool", + }, + "totalItems": Object { + "type": "number", + }, + }, + }, "Content" => Object { "defaultProps": Object { "tagName": "main", diff --git a/packages/react/src/__tests__/index-test.js b/packages/react/src/__tests__/index-test.js index dfb8ea79ae71..8f27e421069e 100644 --- a/packages/react/src/__tests__/index-test.js +++ b/packages/react/src/__tests__/index-test.js @@ -90,6 +90,7 @@ describe('Carbon Components React', () => { "OrderedList", "OverflowMenu", "OverflowMenuItem", + "PageSelector", "Pagination", "PaginationSkeleton", "PrimaryButton", @@ -190,6 +191,7 @@ describe('Carbon Components React', () => { "TooltipDefinition", "TooltipIcon", "UnorderedList", + "Unstable_Pagination", ] `); }); diff --git a/packages/react/src/components/Pagination/Unstable_Pagination/PageSelector.js b/packages/react/src/components/Pagination/Unstable_Pagination/PageSelector.js new file mode 100644 index 000000000000..87ea3244ac5b --- /dev/null +++ b/packages/react/src/components/Pagination/Unstable_Pagination/PageSelector.js @@ -0,0 +1,80 @@ +/** + * Copyright IBM Corp. 2016, 2020 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { settings } from 'carbon-components'; +import setupGetInstanceId from '../../../tools/setupGetInstanceId'; +import Select from '../../Select'; +import SelectItem from '../../SelectItem'; + +const { prefix } = settings; +const getInstanceId = setupGetInstanceId(); + +function PageSelector({ + className, + currentPage, + id, + labelText, + totalPages, + ...other +}) { + const namespace = `${prefix}--unstable-pagination__page-selector`; + const instanceId = `${namespace}__select-${getInstanceId()}`; + + const renderPages = total => { + const pages = []; + for (let counter = 1; counter <= total; counter += 1) { + pages.push( + + ); + } + return pages; + }; + + return ( + + ); +} + +PageSelector.propTypes = { + /** Extra class names to add. */ + className: PropTypes.string, + + /** The current page. */ + currentPage: PropTypes.number.isRequired, + + /** The unique ID of this component instance. */ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + + /** Translatable string to label the page selector element. */ + labelText: PropTypes.string, + + /** + * Total number of pages. + * This value is calculated using a valid `totalItems` prop passed to the parent `Unstable_Pagination`. + */ + totalPages: PropTypes.number.isRequired, +}; + +PageSelector.defaultProps = { + className: null, + id: 1, + labelText: 'Current page number', +}; + +export default PageSelector; diff --git a/packages/react/src/components/Pagination/Unstable_Pagination/Pagination-story.js b/packages/react/src/components/Pagination/Unstable_Pagination/Pagination-story.js new file mode 100644 index 000000000000..70542e7575af --- /dev/null +++ b/packages/react/src/components/Pagination/Unstable_Pagination/Pagination-story.js @@ -0,0 +1,109 @@ +/** + * Copyright IBM Corp. 2016, 2020 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { + array, + boolean, + number, + text, + withKnobs, +} from '@storybook/addon-knobs'; + +import Unstable_Pagination from './Pagination'; +import PageSelector from './PageSelector'; + +const props = () => ({ + disabled: boolean('Disable backward/forward buttons (disabled)', false), + pagesUnknown: boolean('Total number of items unknown (pagesUnknown)', false), + backwardText: text( + 'The description for the backward icon (backwardText)', + 'Previous page' + ), + forwardText: text( + 'The description for the forward icon (forwardText)', + 'Next page' + ), + pageSize: number('Number of items per page (pageSize)', 10), + itemsPerPageText: text( + 'Label for `pageSizes` select UI (itemsPerPageText)', + 'Items per page:' + ), + onChange: action('onChange'), +}); + +storiesOf('UNSTABLE Pagination', module) + .addDecorator(withKnobs) + .addDecorator(story =>
{story()}
) + .add( + 'with a page selector', + () => ( + + {({ currentPage, onSetPage, totalPages }) => ( + onSetPage(event.target.value)} + totalPages={totalPages} + /> + )} + + ), + { + info: { + propTables: [Unstable_Pagination, PageSelector], + text: ` + 🚨 This component is *experimental* and may change. 🚨 + \`Unstable_Pagination\` accepts a render prop \`children\`. + This example wraps the \`children\` (\`PageSelector\`) in a function, allowing it to pass information back to the parent component. + \`\`\`jsx + {/** + * Provide \`totalItems\` to \`Unstable_Pagination\` when using the \`PageSelector\` child. + * \`Unstable_Pagination\` uses \`totalItems\` to calculate \`totalPages\`. + * And then, \`PageSelector\` uses the calculated \`totalPages\` to accurately display page options. + */} + + {/** + * Below, \`children\` is a render prop, wrapped in a function. + * - \`currentPage\` is used to display the current page. + * - \`onSetPage\` is used to update the current page state in the parent component. + * - \`totalPages\` is calculated using the \`totalItems\` value provided to the parent component, and then is displayed below. + */} + {({ currentPage, onSetPage, totalPages }) => ( + onSetPage(event.target.value)} + totalPages={totalPages} + /> + )} + + \`\`\` + `, + }, + } + ) + .add( + 'with no sizer, child input, or child selector', + () => , + { + info: { + text: ` + 🚨 This component is *experimental* and may change. 🚨 + Without \`children\`, \`Unstable_Pagination\` renders without a page selector. + `, + }, + } + ); diff --git a/packages/react/src/components/Pagination/Unstable_Pagination/Pagination.js b/packages/react/src/components/Pagination/Unstable_Pagination/Pagination.js new file mode 100644 index 000000000000..e9d88ce735e7 --- /dev/null +++ b/packages/react/src/components/Pagination/Unstable_Pagination/Pagination.js @@ -0,0 +1,277 @@ +/** + * Copyright IBM Corp. 2016, 2020 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { settings } from 'carbon-components'; +import { CaretRight24, CaretLeft24 } from '@carbon/icons-react'; +import Button from '../../Button'; +import Select from '../../Select'; +import SelectItem from '../../SelectItem'; + +const { prefix } = settings; + +function Unstable_Pagination({ + backwardText, + children, + className, + disabled, + forwardText, + id, + initialPage, + itemsPerPageText, + itemRangeText, + itemText, + pageRangeText, + pageSize, + pageSizes, + pageText, + pagesUnknown, + totalItems, + ...other +}) { + const [currentPage, setCurrentPage] = useState(initialPage); + const [currentPageSize, setCurrentPageSize] = useState(pageSize); + + const totalPages = totalItems + ? Math.max(Math.ceil(totalItems / currentPageSize), 1) + : undefined; + + const backButtonDisabled = disabled || currentPage === 1; + const forwardButtonDisabled = disabled || currentPage === totalPages; + + function onSetPage(newPage) { + setCurrentPage(Number(newPage)); + } + + const namespace = `${prefix}--unstable-pagination`; + + return ( +
+
+ {pageSizes && ( + <> + + + + )} + + <> + {totalItems && + !pagesUnknown && + itemRangeText( + Math.min(currentPageSize * (currentPage - 1) + 1, totalItems), + Math.min(currentPage * currentPageSize, totalItems), + totalItems + )} + + {totalItems && + pagesUnknown && + itemText( + currentPageSize * (currentPage - 1) + 1, + currentPage * currentPageSize + )} + + {!totalItems && + itemText( + currentPageSize * (currentPage - 1) + 1, + currentPage * currentPageSize + )} + + +
+
+ <> + {children && + totalItems && + children({ + currentPage, + currentPageSize, + onSetPage, + totalPages, + })} + + {children && totalItems && !pagesUnknown && ( + + {pageRangeText('', totalPages)} + + )} + + {children && !totalItems && ( + + {pageText(currentPage)} + + )} + + {!children && ( + + {!totalItems + ? pageText(currentPage) + : pageRangeText(currentPage, totalPages)} + + )} + +
+
+ ); +} + +Unstable_Pagination.defaultProps = { + backwardText: 'Previous page', + className: null, + children: undefined, + disabled: false, + forwardText: 'Next page', + id: 1, + itemsPerPageText: 'Items per page:', + itemRangeText: (min, max, total) => `${min}–${max} of ${total} items`, + itemText: (min, max) => `${min}–${max} items`, + initialPage: 1, + pageRangeText: (current, total) => `${current} of ${total} pages`, + pageSize: 10, + pageSizes: undefined, + pageText: page => `page ${page}`, + pagesUnknown: false, + totalItems: undefined, +}; + +Unstable_Pagination.propTypes = { + /** + * The description for the backward icon. + */ + backwardText: PropTypes.string, + + /** + * The children of the pagination component. + */ + children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + + /** + * Extra classes to add. + */ + className: PropTypes.string, + + /** + * `true` if the backward/forward buttons should be disabled. + */ + disabled: PropTypes.bool, + + /** + * The description for the forward icon. + */ + forwardText: PropTypes.string, + + /** The unique ID of this component instance. */ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + + /** + * The translatable text indicating the number of items per page. + */ + itemsPerPageText: PropTypes.string, + + /** + * The function returning a translatable text showing where the current page is, + * in a manner of the range of items. + */ + itemRangeText: PropTypes.func, + + /** + * A variant of `itemRangeText`, used if the total number of items is unknown. + */ + itemText: PropTypes.func, + + /** + * The initial active page when the component is first mounted. + */ + initialPage: PropTypes.number, + + /** + * The function returning a translatable text showing where the current page is, + * in a manner of the total number of pages. + */ + pageRangeText: PropTypes.func, + + /** + * The number dictating how many items a page contains. + */ + pageSize: PropTypes.number, + + /** + * The choices for `pageSize`. + */ + pageSizes: PropTypes.arrayOf(PropTypes.number), + + /** + * The translatable text showing the current page. + */ + pageText: PropTypes.func, + + /** + * `true` if total number of pages is unknown. + */ + pagesUnknown: PropTypes.bool, + + /** + * The total number of items. + * You need to provide total items to calculate total page, + * which is required by a child like the `PageSelector` + * to know how many pages to display. + */ + totalItems: PropTypes.number, +}; + +export default Unstable_Pagination; diff --git a/packages/react/src/components/Pagination/Unstable_Pagination/index.js b/packages/react/src/components/Pagination/Unstable_Pagination/index.js new file mode 100644 index 000000000000..168e64a83098 --- /dev/null +++ b/packages/react/src/components/Pagination/Unstable_Pagination/index.js @@ -0,0 +1,9 @@ +/** + * Copyright IBM Corp. 2016, 2020 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +export Unstable_Pagination from './Pagination'; +export PageSelector from './PageSelector'; diff --git a/packages/react/src/index.js b/packages/react/src/index.js index 686c29db490a..e35511693d61 100644 --- a/packages/react/src/index.js +++ b/packages/react/src/index.js @@ -165,3 +165,9 @@ export ToggleSmallSkeleton from './components/ToggleSmall/ToggleSmall.Skeleton'; export IconSkeleton from './components/Icon/Icon.Skeleton'; export DatePickerSkeleton from './components/DatePicker/DatePicker.Skeleton'; export * from './components/UIShell'; + +// experimental +export { + PageSelector, + Unstable_Pagination, +} from './components/Pagination/Unstable_Pagination';