diff --git a/docs/app/Examples/addons/Pagination/Types/PaginationExamplePagination.js b/docs/app/Examples/addons/Pagination/Types/PaginationExamplePagination.js new file mode 100644 index 0000000000..17c1bbac10 --- /dev/null +++ b/docs/app/Examples/addons/Pagination/Types/PaginationExamplePagination.js @@ -0,0 +1,8 @@ +import React from 'react' +import { Pagination } from 'semantic-ui-react' + +const PaginationExamplePagination = () => ( + +) + +export default PaginationExamplePagination diff --git a/docs/app/Examples/addons/Pagination/Types/index.js b/docs/app/Examples/addons/Pagination/Types/index.js new file mode 100644 index 0000000000..9c50db0a55 --- /dev/null +++ b/docs/app/Examples/addons/Pagination/Types/index.js @@ -0,0 +1,16 @@ +import React from 'react' + +import ComponentExample from 'docs/app/Components/ComponentDoc/ComponentExample' +import ExampleSection from 'docs/app/Components/ComponentDoc/ExampleSection' + +const PaginationTypesExamples = () => ( + + + +) + +export default PaginationTypesExamples diff --git a/docs/app/Examples/addons/Pagination/Usage/PaginationExampleControlled.js b/docs/app/Examples/addons/Pagination/Usage/PaginationExampleControlled.js new file mode 100644 index 0000000000..cedb5f86aa --- /dev/null +++ b/docs/app/Examples/addons/Pagination/Usage/PaginationExampleControlled.js @@ -0,0 +1,28 @@ +import React, { Component } from 'react' +import { Grid, Input, Pagination, Segment } from 'semantic-ui-react' + +export default class PaginationExampleControlled extends Component { + state = { activePage: 1 } + + handleInputChange = (e, { value }) => this.setState({ activePage: value }) + + handlePaginationChange = (e, { activePage }) => this.setState({ activePage }) + + render() { + const { activePage } = this.state + + return ( + + + +
activePage: {activePage}
+ +
+
+ + + +
+ ) + } +} diff --git a/docs/app/Examples/addons/Pagination/Usage/PaginationExampleMenuProps.js b/docs/app/Examples/addons/Pagination/Usage/PaginationExampleMenuProps.js new file mode 100644 index 0000000000..d72d55b816 --- /dev/null +++ b/docs/app/Examples/addons/Pagination/Usage/PaginationExampleMenuProps.js @@ -0,0 +1,15 @@ +import React from 'react' +import { Pagination } from 'semantic-ui-react' + +const PaginationExampleShorthand = () => ( + +) + +export default PaginationExampleShorthand diff --git a/docs/app/Examples/addons/Pagination/Usage/PaginationExampleOptions.js b/docs/app/Examples/addons/Pagination/Usage/PaginationExampleOptions.js new file mode 100644 index 0000000000..b33ecc6cc8 --- /dev/null +++ b/docs/app/Examples/addons/Pagination/Usage/PaginationExampleOptions.js @@ -0,0 +1,114 @@ +import React, { Component } from 'react' +import { Grid, Form, Pagination, Segment } from 'semantic-ui-react' + +export default class PaginationExampleCustomization extends Component { + state = { + activePage: 5, + boundaryRange: 1, + siblingRange: 1, + showEllipsis: true, + showFirstAndLastNav: true, + showPreviousAndNextNav: true, + totalPages: 50, + } + + handleCheckboxChange = (e, { checked, name }) => this.setState({ [name]: checked }) + + handleInputChange = (e, { name, value }) => this.setState({ [name]: value }) + + handlePaginationChange = (e, { activePage }) => this.setState({ activePage }) + + render() { + const { + activePage, + boundaryRange, + siblingRange, + showEllipsis, + showFirstAndLastNav, + showPreviousAndNextNav, + totalPages, + } = this.state + + return ( + + + + + + +
+ + + + + + + + + + + + + +
+
+ +
+ ) + } +} diff --git a/docs/app/Examples/addons/Pagination/Usage/PaginationExampleShorthand.js b/docs/app/Examples/addons/Pagination/Usage/PaginationExampleShorthand.js new file mode 100644 index 0000000000..5ead316115 --- /dev/null +++ b/docs/app/Examples/addons/Pagination/Usage/PaginationExampleShorthand.js @@ -0,0 +1,16 @@ +import React from 'react' +import { Icon, Pagination } from 'semantic-ui-react' + +const PaginationExampleShorthand = () => ( + , icon: true }} + firstItem={{ content: , icon: true }} + lastItem={{ content: , icon: true }} + prevItem={{ content: , icon: true }} + nextItem={{ content: , icon: true }} + totalPages={10} + /> +) + +export default PaginationExampleShorthand diff --git a/docs/app/Examples/addons/Pagination/Usage/index.js b/docs/app/Examples/addons/Pagination/Usage/index.js new file mode 100644 index 0000000000..9cd0cd9bd2 --- /dev/null +++ b/docs/app/Examples/addons/Pagination/Usage/index.js @@ -0,0 +1,31 @@ +import React from 'react' + +import ComponentExample from 'docs/app/Components/ComponentDoc/ComponentExample' +import ExampleSection from 'docs/app/Components/ComponentDoc/ExampleSection' + +const PaginationUsageExamples = () => ( + + + + + + +) + +export default PaginationUsageExamples diff --git a/docs/app/Examples/addons/Pagination/index.js b/docs/app/Examples/addons/Pagination/index.js new file mode 100644 index 0000000000..5240229255 --- /dev/null +++ b/docs/app/Examples/addons/Pagination/index.js @@ -0,0 +1,13 @@ +import React from 'react' + +import Types from './Types' +import Usage from './Usage' + +const PaginationExamples = () => ( +
+ + +
+) + +export default PaginationExamples diff --git a/docs/app/Examples/collections/Menu/Types/index.js b/docs/app/Examples/collections/Menu/Types/index.js index ca12fe5cf6..12f9021529 100644 --- a/docs/app/Examples/collections/Menu/Types/index.js +++ b/docs/app/Examples/collections/Menu/Types/index.js @@ -1,4 +1,6 @@ import React from 'react' +import { Link } from 'react-router-dom' +import { Message } from 'semantic-ui-react' import ExampleSection from 'docs/app/Components/ComponentDoc/ExampleSection' import ComponentExample from 'docs/app/Components/ComponentDoc/ComponentExample' @@ -67,7 +69,11 @@ const Types = () => ( title='Pagination' description='A pagination menu is specially formatted to present links to pages of content.' examplePath='collections/Menu/Types/MenuExamplePagination' - /> + > + + For fully featured pagination, see Pagination addon. + + ) diff --git a/index.d.ts b/index.d.ts index c17237711d..a983f152ab 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,13 +1,15 @@ // Addons +export { default as Confirm, ConfirmProps } from './dist/commonjs/addons/Confirm'; +export { default as Pagination, PaginationProps } from './dist/commonjs/addons/Pagination'; +export { default as PaginationItem, PaginationItemProps } from './dist/commonjs/addons/Pagination/PaginationItem'; +export { default as Portal, PortalProps } from './dist/commonjs/addons/Portal'; +export { default as Radio, RadioProps } from './dist/commonjs/addons/Radio'; +export { default as Ref, RefProps } from './dist/commonjs/addons/Ref'; export { default as Responsive, ResponsiveProps, ResponsiveWidthShorthand } from './dist/commonjs/addons/Responsive'; -export { default as Confirm, ConfirmProps } from './dist/commonjs/addons/Confirm'; -export { default as Portal, PortalProps } from './dist/commonjs/addons/Portal'; -export { default as Radio, RadioProps } from './dist/commonjs/addons/Radio'; -export { default as Ref, RefProps } from './dist/commonjs/addons/Ref'; export { default as Select, SelectProps } from './dist/commonjs/addons/Select'; export { default as TextArea, TextAreaProps } from './dist/commonjs/addons/TextArea'; export { diff --git a/src/addons/Pagination/Pagination.d.ts b/src/addons/Pagination/Pagination.d.ts new file mode 100644 index 0000000000..1cfd61e699 --- /dev/null +++ b/src/addons/Pagination/Pagination.d.ts @@ -0,0 +1,58 @@ +import * as React from 'react'; + +import { SemanticShorthandItem } from '../..'; +import { default as PaginationItem, PaginationItemProps } from './PaginationItem'; + +export interface PaginationProps { + [key: string]: any; + + /** A pagination item can have an aria label. */ + ariaLabel?: string; + + /** Initial activePage value. */ + defaultActivePage?: number | string; + + /** Index of the currently active page. */ + activePage?: number | string; + + /** Number of always visible pages at the beginning and end. */ + boundaryRange?: number | string; + + /** A shorthand for PaginationItem. */ + ellipsisItem?: SemanticShorthandItem; + + /** A shorthand for PaginationItem. */ + firstItem?: SemanticShorthandItem; + + /** A shorthand for PaginationItem. */ + lastItem?: SemanticShorthandItem; + + /** A shorthand for PaginationItem. */ + nextItem?: SemanticShorthandItem; + + /** A shorthand for PaginationItem. */ + pageItem?: SemanticShorthandItem; + + /** A shorthand for PaginationItem. */ + prevItem?: SemanticShorthandItem; + + /** + * Called on change of an active page. + * + * @param {SyntheticEvent} event - React's original SyntheticEvent. + * @param {object} data - All props. + */ + onPageChange?: (event: React.MouseEvent, data: PaginationProps) => void; + + /** Number of always visible pages before and after the current one. */ + siblingRange?: number | string; + + /** Total number of pages. */ + totalPages: number | string; +} + +declare class Pagination extends React.Component { + static Item: typeof PaginationItem; +} + +export default Pagination; diff --git a/src/addons/Pagination/Pagination.js b/src/addons/Pagination/Pagination.js new file mode 100644 index 0000000000..befe90750b --- /dev/null +++ b/src/addons/Pagination/Pagination.js @@ -0,0 +1,149 @@ +import _ from 'lodash' +import PropTypes from 'prop-types' +import React from 'react' + +import { + AutoControlledComponent as Component, + createPaginationItems, + customPropTypes, + getUnhandledProps, + META, +} from '../../lib' +import Menu from '../../collections/Menu' +import PaginationItem from './PaginationItem' + +/** + * A component to render a pagination. + */ +export default class Pagination extends Component { + static propTypes = { + /** A pagination item can have an aria label. */ + ariaLabel: PropTypes.string, + + /** Initial activePage value. */ + defaultActivePage: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]), + + /** Index of the currently active page. */ + activePage: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]), + + /** Number of always visible pages at the beginning and end. */ + boundaryRange: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]), + + /** A shorthand for PaginationItem. */ + ellipsisItem: customPropTypes.itemShorthand, + + /** A shorthand for PaginationItem. */ + firstItem: customPropTypes.itemShorthand, + + /** A shorthand for PaginationItem. */ + lastItem: customPropTypes.itemShorthand, + + /** A shorthand for PaginationItem. */ + nextItem: customPropTypes.itemShorthand, + + /** A shorthand for PaginationItem. */ + pageItem: customPropTypes.itemShorthand, + + /** A shorthand for PaginationItem. */ + prevItem: customPropTypes.itemShorthand, + + /** + * Called on change of an active page. + * + * @param {SyntheticEvent} event - React's original SyntheticEvent. + * @param {object} data - All props. + */ + onPageChange: PropTypes.func, + + /** Number of always visible pages before and after the current one. */ + siblingRange: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]), + + /** Total number of pages. */ + totalPages: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]).isRequired, + } + + static autoControlledProps = [ + 'activePage', + ] + + static defaultProps = { + ariaLabel: 'Pagination Navigation', + boundaryRange: 1, + ellipsisItem: '...', + firstItem: { + ariaLabel: 'First item', + content: '«', + }, + lastItem: { + ariaLabel: 'Last item', + content: '»', + }, + nextItem: { + ariaLabel: 'Next item', + content: '⟩', + }, + pageItem: {}, + prevItem: { + ariaLabel: 'Previous item', + content: '⟨', + }, + siblingRange: 1, + } + + static _meta = { + name: 'Pagination', + type: META.TYPES.ADDON, + } + + static Item = PaginationItem + + handleItemClick = (e, { value }) => { + this.trySetState({ activePage: value }) + _.invoke(this.props, 'onPageChange', e, { ...this.props, activePage: value }) + } + + handleItemOverrides = (active, type, value) => predefinedProps => ({ + active, + type, + key: `${type}-${value}`, + onClick: (e, itemProps) => { + _.invoke(predefinedProps, 'onClick', e, itemProps) + this.handleItemClick(e, itemProps) + }, + }) + + render() { + const { ariaLabel, boundaryRange, siblingRange, totalPages } = this.props + const { activePage } = this.state + + const items = createPaginationItems({ activePage, boundaryRange, siblingRange, totalPages }) + const rest = getUnhandledProps(Pagination, this.props) + + return ( + + {_.map(items, ({ active, type, value }) => PaginationItem.create(this.props[type], { + defaultProps: { + content: value, + value, + }, + overrideProps: this.handleItemOverrides(active, type, value), + }))} + + ) + } +} diff --git a/src/addons/Pagination/PaginationItem.d.ts b/src/addons/Pagination/PaginationItem.d.ts new file mode 100644 index 0000000000..9777c65709 --- /dev/null +++ b/src/addons/Pagination/PaginationItem.d.ts @@ -0,0 +1,35 @@ +import * as React from 'react'; + +export interface PaginationItemProps { + [key: string]: any; + + /** A pagination item can be active. */ + active?: boolean; + + /** A pagination item can have an aria label. */ + ariaLabel?: string; + + /** + * Called on click. + * + * @param {SyntheticEvent} event - React's original SyntheticEvent. + * @param {object} data - All props. + */ + onClick?: (event: React.MouseEvent, data: PaginationItemProps) => void; + + /** + * Called on key down. + * + * @param {SyntheticEvent} event - React's original SyntheticEvent. + * @param {object} data - All props. + */ + onKeyDown?: (event: React.MouseEvent, data: PaginationItemProps) => void; + + /** A pagination should have a type. */ + type?: 'ellipsisItem' | 'firstItem' | 'prevItem' | 'pageItem' | 'nextItem' | 'lastItem'; +} + +declare class PaginationItem extends React.Component { +} + +export default PaginationItem; diff --git a/src/addons/Pagination/PaginationItem.js b/src/addons/Pagination/PaginationItem.js new file mode 100644 index 0000000000..dd01571ec3 --- /dev/null +++ b/src/addons/Pagination/PaginationItem.js @@ -0,0 +1,86 @@ +import _ from 'lodash' +import PropTypes from 'prop-types' +import { Component } from 'react' + +import { + createShorthandFactory, + keyboardKey, + META, +} from '../../lib' +import MenuItem from '../../collections/Menu/MenuItem' + +/** + * An item of a pagination. + */ +class PaginationItem extends Component { + static propTypes = { + /** A pagination item can be active. */ + active: PropTypes.bool, + + /** A pagination item can have an aria label. */ + ariaLabel: PropTypes.string, + + /** + * Called on click. + * + * @param {SyntheticEvent} event - React's original SyntheticEvent. + * @param {object} data - All props. + */ + onClick: PropTypes.func, + + /** + * Called on key down. + * + * @param {SyntheticEvent} event - React's original SyntheticEvent. + * @param {object} data - All props. + */ + onKeyDown: PropTypes.func, + + /** A pagination should have a type. */ + type: PropTypes.oneOf([ + 'ellipsisItem', + 'firstItem', + 'prevItem', + 'pageItem', + 'nextItem', + 'lastItem', + ]), + } + + static _meta = { + name: 'PaginationItem', + parent: 'Pagination', + type: META.TYPES.ADDON, + } + + handleClick = (e) => { + const { type } = this.props + + if (type !== 'ellipsisItem') _.invoke(this.props, 'onClick', e, this.props) + } + + handleKeyDown = (e) => { + _.invoke(this.props, 'onKeyDown', e, this.props) + if (keyboardKey.getCode(e) === keyboardKey.Enter) _.invoke(this.props, 'onClick', e, this.props) + } + + render() { + const { active, ariaLabel, type, ...rest } = this.props + const disabled = type === 'ellipsisItem' + + return MenuItem.create({ + ...rest, + active, + 'aria-current': active, + 'aria-label': ariaLabel, + disabled, + onClick: this.handleClick, + onKeyDown: this.handleKeyDown, + tabIndex: disabled ? -1 : 0, + }) + } +} + +PaginationItem.create = createShorthandFactory(PaginationItem, content => ({ content })) + +export default PaginationItem diff --git a/src/addons/Pagination/index.d.ts b/src/addons/Pagination/index.d.ts new file mode 100644 index 0000000000..80fe83512b --- /dev/null +++ b/src/addons/Pagination/index.d.ts @@ -0,0 +1 @@ +export { default, PaginationProps } from './Pagination'; diff --git a/src/addons/Pagination/index.js b/src/addons/Pagination/index.js new file mode 100644 index 0000000000..b0218a3411 --- /dev/null +++ b/src/addons/Pagination/index.js @@ -0,0 +1 @@ +export default from './Pagination' diff --git a/src/index.js b/src/index.js index b6c27c97cb..4ce1bbd9ef 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,11 @@ // Addons -export { default as Responsive } from './addons/Responsive' export { default as Confirm } from './addons/Confirm' +export { default as Pagination } from './addons/Pagination' +export { default as PaginationItem } from './addons/Pagination/PaginationItem' export { default as Portal } from './addons/Portal' export { default as Radio } from './addons/Radio' export { default as Ref } from './addons/Ref' +export { default as Responsive } from './addons/Responsive' export { default as Select } from './addons/Select' export { default as TextArea } from './addons/TextArea' export { default as TransitionablePortal } from './addons/TransitionablePortal' diff --git a/src/lib/createPaginationItems/createPaginationItems.js b/src/lib/createPaginationItems/createPaginationItems.js new file mode 100644 index 0000000000..f4a3894146 --- /dev/null +++ b/src/lib/createPaginationItems/createPaginationItems.js @@ -0,0 +1,36 @@ +import { + createFirstPage, + createLastItem, + createNextItem, + createPageFactory, + createPrevItem, +} from './itemFactories' +import { createComplexRange, createSimpleRange } from './rangeFactories' +import { isSimplePagination, typifyOptions } from './paginationUtils' + +/** + * @param {object} rawOptions + * @param {number} rawOptions.activePage + * @param {number} rawOptions.boundaryRange Number of always visible pages at the beginning and end. + * @param {number} rawOptions.siblingRange Number of always visible pages before and after the current one. + * @param {number} rawOptions.totalPages Total number of pages. + */ +const createPaginationItems = (rawOptions) => { + const options = typifyOptions(rawOptions) + const { activePage, totalPages } = options + + const pageFactory = createPageFactory(activePage) + const innerRange = isSimplePagination(options) + ? createSimpleRange(1, totalPages, pageFactory) + : createComplexRange(options, pageFactory) + + return [ + createFirstPage(), + createPrevItem(activePage), + ...innerRange, + createNextItem(activePage, totalPages), + createLastItem(totalPages), + ] +} + +export default createPaginationItems diff --git a/src/lib/createPaginationItems/index.js b/src/lib/createPaginationItems/index.js new file mode 100644 index 0000000000..01df3cfb93 --- /dev/null +++ b/src/lib/createPaginationItems/index.js @@ -0,0 +1 @@ +export default from './createPaginationItems' diff --git a/src/lib/createPaginationItems/itemFactories.js b/src/lib/createPaginationItems/itemFactories.js new file mode 100644 index 0000000000..b4a7ba6ebb --- /dev/null +++ b/src/lib/createPaginationItems/itemFactories.js @@ -0,0 +1,59 @@ +/** + * @param {number} pageNumber + * @return {Object} + */ +export const createEllipsisItem = pageNumber => ({ + active: false, + type: 'ellipsisItem', + value: pageNumber, +}) + +/** + * @return {Object} + */ +export const createFirstPage = () => ({ + active: false, + type: 'firstItem', + value: 1, +}) + +/** + * @param {number} activePage + * @return {Object} + */ +export const createPrevItem = activePage => ({ + active: false, + type: 'prevItem', + value: Math.max(1, activePage - 1), +}) + +/** + * @param {number} activePage + * @return {function} + */ +export const createPageFactory = activePage => pageNumber => ({ + active: activePage === pageNumber, + type: 'pageItem', + value: pageNumber, +}) + +/** + * @param {number} activePage + * @param {number} totalPages + * @return {Object} + */ +export const createNextItem = (activePage, totalPages) => ({ + active: false, + type: 'nextItem', + value: Math.min(activePage + 1, totalPages), +}) + +/** + * @param {number} totalPages + * @return {Object} + */ +export const createLastItem = totalPages => ({ + active: false, + type: 'lastItem', + value: totalPages, +}) diff --git a/src/lib/createPaginationItems/paginationUtils.js b/src/lib/createPaginationItems/paginationUtils.js new file mode 100644 index 0000000000..492b85203d --- /dev/null +++ b/src/lib/createPaginationItems/paginationUtils.js @@ -0,0 +1,29 @@ +/** + * Checks the possibility of using simple range generation, if number of generated pages is equal + * or greater than total pages to show. + * + * @param {object} options + * @param {number} options.boundaryRange Number of always visible pages at the beginning and end. + * @param {number} options.siblingRange Number of always visible pages before and after the current one. + * @param {number} options.totalPages Total number of pages. + * @return {boolean} + */ +export const isSimplePagination = ({ boundaryRange, siblingRange, totalPages }) => { + const boundaryRangeSize = 2 * boundaryRange + const ellipsisSize = 2 + const siblingRangeSize = 2 * siblingRange + + return 1 + ellipsisSize + siblingRangeSize + boundaryRangeSize >= totalPages +} + +export const typifyOptions = ({ + activePage, + boundaryRange, + siblingRange, + totalPages, +}) => ({ + activePage: +activePage, + boundaryRange: +boundaryRange, + siblingRange: +siblingRange, + totalPages: +totalPages, +}) diff --git a/src/lib/createPaginationItems/rangeFactories.js b/src/lib/createPaginationItems/rangeFactories.js new file mode 100644 index 0000000000..d96430f226 --- /dev/null +++ b/src/lib/createPaginationItems/rangeFactories.js @@ -0,0 +1,29 @@ +import _ from 'lodash' +import { createInnerPrefix, createInnerSuffix } from './suffixFactories' + +export const createSimpleRange = (start, end, pageFactory) => _.map(_.range(start, end + 1), pageFactory) + +export const createComplexRange = (options, pageFactory) => { + const { activePage, boundaryRange, siblingRange, totalPages } = options + + const firstGroupEnd = boundaryRange + const firstGroup = createSimpleRange(1, firstGroupEnd, pageFactory) + + const lastGroupStart = (totalPages + 1) - boundaryRange + const lastGroup = createSimpleRange(lastGroupStart, totalPages, pageFactory) + + const innerGroupStart = Math.min( + Math.max(activePage - siblingRange, firstGroupEnd + 2), + lastGroupStart - 1 - (2 * siblingRange) - 1, + ) + const innerGroupEnd = innerGroupStart + (2 * siblingRange) + const innerGroup = createSimpleRange(innerGroupStart, innerGroupEnd, pageFactory) + + return [ + ...firstGroup, + createInnerPrefix(firstGroupEnd, innerGroupStart, pageFactory), + ...innerGroup, + createInnerSuffix(innerGroupEnd, lastGroupStart, pageFactory), + ...lastGroup, + ].filter(Boolean) +} diff --git a/src/lib/createPaginationItems/suffixFactories.js b/src/lib/createPaginationItems/suffixFactories.js new file mode 100644 index 0000000000..1a181e8bb6 --- /dev/null +++ b/src/lib/createPaginationItems/suffixFactories.js @@ -0,0 +1,17 @@ +import { createEllipsisItem } from './itemFactories' + +export const createInnerPrefix = (firstGroupEnd, innerGroupStart, pageFactory) => { + const prefixPage = innerGroupStart - 1 + const showEllipsis = prefixPage !== (firstGroupEnd + 1) + const prefixFactory = showEllipsis ? createEllipsisItem : pageFactory + + return prefixFactory(prefixPage) +} + +export const createInnerSuffix = (innerGroupEnd, lastGroupStart, pageFactory) => { + const suffixPage = innerGroupEnd + 1 + const showEllipsis = suffixPage !== (lastGroupStart - 1) + const suffixFactory = showEllipsis ? createEllipsisItem : pageFactory + + return suffixFactory(suffixPage) +} diff --git a/src/lib/index.js b/src/lib/index.js index f6f506a619..ca583b905c 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -35,6 +35,7 @@ export { export { default as isBrowser } from './isBrowser' export { default as leven } from './leven' export * as META from './META' +export createPaginationItems from './createPaginationItems' export * as SUI from './SUI' export { default as keyboardKey } from './keyboardKey' diff --git a/test/specs/addons/Pagination/Pagination-test.js b/test/specs/addons/Pagination/Pagination-test.js new file mode 100644 index 0000000000..ab5d768fc0 --- /dev/null +++ b/test/specs/addons/Pagination/Pagination-test.js @@ -0,0 +1,38 @@ +import React from 'react' + +import Pagination from 'src/addons/Pagination/Pagination' +import PaginationItem from 'src/addons/Pagination/PaginationItem' +import * as common from 'test/specs/commonTests' +import { sandbox } from 'test/utils' + +describe('Pagination', () => { + common.isConformant(Pagination, { requiredProps: { + totalPages: 0, + } }) + common.hasSubComponents(Pagination, [PaginationItem]) + + describe('onPageChange', () => { + it('is called with (e, data) when clicked on a pagination item', () => { + const event = { target: null } + const onPageChange = sandbox.spy() + const onPageItemClick = sandbox.spy() + + mount( + , + ) + .find('PaginationItem') + .at(4) + .simulate('click', event) + + onPageChange.should.have.been.calledOnce() + onPageChange.should.have.been.calledWithMatch(event, { activePage: 3 }) + onPageItemClick.should.have.been.calledOnce() + onPageItemClick.should.have.been.calledWithMatch(event, { value: 3 }) + }) + }) +}) diff --git a/test/specs/addons/Pagination/PaginationItem-test.js b/test/specs/addons/Pagination/PaginationItem-test.js new file mode 100644 index 0000000000..1f8292a7fd --- /dev/null +++ b/test/specs/addons/Pagination/PaginationItem-test.js @@ -0,0 +1,81 @@ +import React from 'react' + +import PaginationItem from 'src/addons/Pagination/PaginationItem' +import * as common from 'test/specs/commonTests' +import { sandbox } from 'test/utils' + +describe('PaginationItem', () => { + common.isConformant(PaginationItem) + common.implementsCreateMethod(PaginationItem) + + describe('disabled', () => { + it('is false by default', () => { + shallow() + .should.have.prop('disabled', false) + }) + + it('is true when "type" is "ellipsisItem"', () => { + shallow() + .should.have.prop('disabled', true) + }) + }) + + describe('onClick', () => { + it('is called with (e, props) when clicked', () => { + const event = { target: null } + const onClick = sandbox.spy() + + shallow() + .simulate('click', event) + + onClick.should.have.been.calledOnce() + onClick.should.have.been.calledWithMatch(event, { onClick }) + }) + + it('is called with (e, props) when "Enter" is pressed', () => { + const event = { key: 'Enter', target: null } + const onClick = sandbox.spy() + + shallow() + .simulate('keyDown', event) + + onClick.should.have.been.calledOnce() + onClick.should.have.been.calledWithMatch(event, { onClick }) + }) + + it('is omitted when "type" is "ellipsisItem"', () => { + const event = { target: null } + const onClick = sandbox.spy() + + shallow() + .simulate('click', event) + + onClick.should.have.been.not.called() + }) + }) + + describe('onKeyDown', () => { + it('is called with (e, props) when clicked', () => { + const event = { key: 'Enter', target: null } + const onKeyDown = sandbox.spy() + + shallow() + .simulate('keyDown', event) + + onKeyDown.should.have.been.calledOnce() + onKeyDown.should.have.been.calledWithMatch(event, { onKeyDown }) + }) + }) + + describe('tabIndex', () => { + it('is 0 by default', () => { + shallow() + .should.have.prop('tabIndex', 0) + }) + + it('is -1 when "type" is "ellipsisItem"', () => { + shallow() + .should.have.prop('tabIndex', -1) + }) + }) +}) diff --git a/test/specs/lib/createPaginationItems/createPaginationItems-test.js b/test/specs/lib/createPaginationItems/createPaginationItems-test.js new file mode 100644 index 0000000000..3bea0166cd --- /dev/null +++ b/test/specs/lib/createPaginationItems/createPaginationItems-test.js @@ -0,0 +1,33 @@ +import createPaginationItems from 'src/lib/createPaginationItems' + +describe('createPaginationItems', () => { + it('creates an array of item objects', () => { + createPaginationItems({ + activePage: 15, + boundaryRange: 2, + siblingRange: 2, + totalPages: 30, + }).should.deep.equal([ + { active: false, type: 'firstItem', value: 1 }, + { active: false, type: 'prevItem', value: 14 }, + + { active: false, type: 'pageItem', value: 1 }, + { active: false, type: 'pageItem', value: 2 }, + + { active: false, type: 'ellipsisItem', value: 12 }, + { active: false, type: 'pageItem', value: 13 }, + { active: false, type: 'pageItem', value: 14 }, + { active: true, type: 'pageItem', value: 15 }, + { active: false, type: 'pageItem', value: 16 }, + { active: false, type: 'pageItem', value: 17 }, + { active: false, type: 'ellipsisItem', value: 18 }, + + { active: false, type: 'pageItem', value: 29 }, + { active: false, type: 'pageItem', value: 30 }, + + { active: false, type: 'nextItem', value: 16 }, + { active: false, type: 'lastItem', value: 30 }, + ]) + }) +}) + diff --git a/test/specs/lib/createPaginationItems/itemFactories-test.js b/test/specs/lib/createPaginationItems/itemFactories-test.js new file mode 100644 index 0000000000..5f437448a0 --- /dev/null +++ b/test/specs/lib/createPaginationItems/itemFactories-test.js @@ -0,0 +1,95 @@ +import { + createEllipsisItem, + createFirstPage, + createLastItem, + createNextItem, + createPageFactory, + createPrevItem, +} from 'src/lib/createPaginationItems/itemFactories' + +describe('itemFactories', () => { + describe('createEllipsisItem', () => { + it('"active" is always false', () => { + createEllipsisItem(0).should.have.property('active', false) + }) + it('"type" matches "ellipsisItem"', () => { + createEllipsisItem(0).should.have.property('type', 'ellipsisItem') + }) + it('"value" matches passed argument', () => { + createEllipsisItem(5).should.have.property('value', 5) + }) + }) + + describe('createFirstPage', () => { + it('"active" is always false', () => { + createFirstPage().should.have.property('active', false) + }) + it('"type" matches "firstItem"', () => { + createFirstPage().should.have.property('type', 'firstItem') + }) + it('"value" always returns 1', () => { + createFirstPage().should.have.property('value', 1) + }) + }) + + describe('createPrevItem', () => { + it('"active" is always false', () => { + createPrevItem(1).should.have.property('active', false) + }) + it('"type" matches "prevItem"', () => { + createPrevItem(1).should.have.property('type', 'prevItem') + }) + it('"value" returns previous page number or 1', () => { + createPrevItem(1).should.have.property('value', 1) + createPrevItem(2).should.have.property('value', 1) + createPrevItem(3).should.have.property('value', 2) + }) + }) + + describe('createPageFactory', () => { + const pageFactory = createPageFactory(1) + + it('returns function', () => { + pageFactory.should.be.a('function') + }) + it('"active" is true when pageNumber is equal to activePage', () => { + pageFactory(1).should.have.property('active', true) + }) + it('"active" is false when pageNumber is not equal to activePage', () => { + pageFactory(2).should.have.property('active', false) + }) + it('"type" of created item matches "pageItem"', () => { + pageFactory(2).should.have.property('type', 'pageItem') + }) + it('"value" returns pageNumber', () => { + pageFactory(1).should.have.property('value', 1) + pageFactory(2).should.have.property('value', 2) + }) + }) + + describe('createNextItem', () => { + it('"active" is always false', () => { + createNextItem(0, 0).should.have.property('active', false) + }) + it('"type" matches "nextItem"', () => { + createNextItem(0, 0).should.have.property('type', 'nextItem') + }) + it('"value" returns the smallest of the arguments', () => { + createNextItem(1, 3).should.have.property('value', 2) + createNextItem(2, 3).should.have.property('value', 3) + createNextItem(3, 3).should.have.property('value', 3) + }) + }) + + describe('createLastItem', () => { + it('"active" is always false', () => { + createLastItem(0).should.have.property('active', false) + }) + it('"type" matches "lastItem"', () => { + createLastItem(0).should.have.property('type', 'lastItem') + }) + it('"value" matches passed argument', () => { + createLastItem(2).should.have.property('value', 2) + }) + }) +}) diff --git a/test/specs/lib/createPaginationItems/paginationUtils-test.js b/test/specs/lib/createPaginationItems/paginationUtils-test.js new file mode 100644 index 0000000000..01b2595299 --- /dev/null +++ b/test/specs/lib/createPaginationItems/paginationUtils-test.js @@ -0,0 +1,33 @@ +import { createInnerPrefix, createInnerSuffix } from 'src/lib/createPaginationItems/suffixFactories' +import { sandbox } from 'test/utils' + +describe('suffixFactories', () => { + describe('createInnerPrefix', () => { + it('returns ellipsisItem when is outside innerGroup', () => { + createInnerPrefix(5, 10, () => {}) + .should.have.property('type', 'ellipsisItem') + }) + + it('calls pageFactory when position matches border of a group', () => { + const pageFactory = sandbox.spy() + createInnerPrefix(5, 7, pageFactory) + + pageFactory.should.have.been.calledOnce() + }) + }) + + describe('createInnerSuffix', () => { + it('returns ellipsisItem when is outside innerGroup', () => { + createInnerSuffix(5, 10, () => {}) + .should.have.property('type', 'ellipsisItem') + }) + + it('calls pageFactory when position matches border of a group', () => { + const pageFactory = sandbox.spy() + createInnerSuffix(5, 7, pageFactory) + + pageFactory.should.have.been.calledOnce() + }) + }) +}) + diff --git a/test/specs/lib/createPaginationItems/rangeFactories-test.js b/test/specs/lib/createPaginationItems/rangeFactories-test.js new file mode 100644 index 0000000000..042472cf2f --- /dev/null +++ b/test/specs/lib/createPaginationItems/rangeFactories-test.js @@ -0,0 +1,14 @@ +import { createSimpleRange } from 'src/lib/createPaginationItems/rangeFactories' +import { sandbox } from 'test/utils' + +describe('rangeFactories', () => { + describe('createSimpleRange', () => { + it('creates range and calls pageFactory', () => { + const pageFactory = sandbox.spy() + createSimpleRange(5, 10, pageFactory) + + pageFactory.should.have.callCount(6) + }) + }) +}) + diff --git a/test/specs/lib/createPaginationItems/suffixFactories-test.js b/test/specs/lib/createPaginationItems/suffixFactories-test.js new file mode 100644 index 0000000000..01b2595299 --- /dev/null +++ b/test/specs/lib/createPaginationItems/suffixFactories-test.js @@ -0,0 +1,33 @@ +import { createInnerPrefix, createInnerSuffix } from 'src/lib/createPaginationItems/suffixFactories' +import { sandbox } from 'test/utils' + +describe('suffixFactories', () => { + describe('createInnerPrefix', () => { + it('returns ellipsisItem when is outside innerGroup', () => { + createInnerPrefix(5, 10, () => {}) + .should.have.property('type', 'ellipsisItem') + }) + + it('calls pageFactory when position matches border of a group', () => { + const pageFactory = sandbox.spy() + createInnerPrefix(5, 7, pageFactory) + + pageFactory.should.have.been.calledOnce() + }) + }) + + describe('createInnerSuffix', () => { + it('returns ellipsisItem when is outside innerGroup', () => { + createInnerSuffix(5, 10, () => {}) + .should.have.property('type', 'ellipsisItem') + }) + + it('calls pageFactory when position matches border of a group', () => { + const pageFactory = sandbox.spy() + createInnerSuffix(5, 7, pageFactory) + + pageFactory.should.have.been.calledOnce() + }) + }) +}) +