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 (
+
+ )
+ }
+}
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()
+ })
+ })
+})
+