diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-05-24 19:35:12 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-05-24 19:35:12 +0200 |
| commit | c85ab5ad43ccf52881ee224672c41ec30021cf48 (patch) | |
| tree | 8058808d9bfca19383f120c46b34d99ff2f89f63 /src/components/molecules/nav | |
| parent | 52404177c07a2aab7fc894362fb3060dff2431a0 (diff) | |
| parent | 11b9de44a4b2f305a6a484187805e429b2767118 (diff) | |
refactor: use storybook and atomic design (#16)
BREAKING CHANGE: rewrite most of the Typescript types, so the content format (the meta in particular) needs to be updated.
Diffstat (limited to 'src/components/molecules/nav')
| -rw-r--r-- | src/components/molecules/nav/breadcrumb.module.scss | 19 | ||||
| -rw-r--r-- | src/components/molecules/nav/breadcrumb.stories.tsx | 81 | ||||
| -rw-r--r-- | src/components/molecules/nav/breadcrumb.test.tsx | 15 | ||||
| -rw-r--r-- | src/components/molecules/nav/breadcrumb.tsx | 127 | ||||
| -rw-r--r-- | src/components/molecules/nav/nav.module.scss | 22 | ||||
| -rw-r--r-- | src/components/molecules/nav/nav.stories.tsx | 107 | ||||
| -rw-r--r-- | src/components/molecules/nav/nav.test.tsx | 28 | ||||
| -rw-r--r-- | src/components/molecules/nav/nav.tsx | 85 | ||||
| -rw-r--r-- | src/components/molecules/nav/pagination.module.scss | 51 | ||||
| -rw-r--r-- | src/components/molecules/nav/pagination.stories.tsx | 171 | ||||
| -rw-r--r-- | src/components/molecules/nav/pagination.test.tsx | 26 | ||||
| -rw-r--r-- | src/components/molecules/nav/pagination.tsx | 220 |
12 files changed, 952 insertions, 0 deletions
diff --git a/src/components/molecules/nav/breadcrumb.module.scss b/src/components/molecules/nav/breadcrumb.module.scss new file mode 100644 index 0000000..c26f60a --- /dev/null +++ b/src/components/molecules/nav/breadcrumb.module.scss @@ -0,0 +1,19 @@ +@use "@styles/abstracts/placeholders"; + +.list { + @extend %reset-ordered-list; + + display: flex; + flex-flow: row wrap; + align-items: center; + gap: var(--spacing-2xs); +} + +.item { + &:not(:last-of-type) { + &::after { + content: ">"; + margin-left: var(--spacing-2xs); + } + } +} diff --git a/src/components/molecules/nav/breadcrumb.stories.tsx b/src/components/molecules/nav/breadcrumb.stories.tsx new file mode 100644 index 0000000..cf67e60 --- /dev/null +++ b/src/components/molecules/nav/breadcrumb.stories.tsx @@ -0,0 +1,81 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import Breadcrumb from './breadcrumb'; + +/** + * Breadcrumb - Storybook Meta + */ +export default { + title: 'Molecules/Navigation/Breadcrumb', + component: Breadcrumb, + argTypes: { + className: { + control: { + type: 'text', + }, + table: { + category: 'Styles', + }, + description: 'Set additional classnames to the nav element.', + type: { + name: 'string', + required: false, + }, + }, + itemClassName: { + control: { + type: 'text', + }, + table: { + category: 'Styles', + }, + description: 'Set additional classnames to the breadcrumb items.', + type: { + name: 'string', + required: false, + }, + }, + items: { + description: 'The breadcrumb items.', + type: { + name: 'object', + required: true, + value: {}, + }, + }, + }, +} as ComponentMeta<typeof Breadcrumb>; + +const Template: ComponentStory<typeof Breadcrumb> = (args) => ( + <Breadcrumb {...args} /> +); + +/** + * Breadcrumb Stories - One item + */ +export const OneItem = Template.bind({}); +OneItem.args = { + items: [{ id: 'home', url: '#', name: 'Home' }], +}; + +/** + * Breadcrumb Stories - Two items + */ +export const TwoItems = Template.bind({}); +TwoItems.args = { + items: [ + { id: 'home', url: '#', name: 'Home' }, + { id: 'blog', url: '#', name: 'Blog' }, + ], +}; + +/** + * Breadcrumb Stories - Three items + */ +export const ThreeItems = Template.bind({}); +ThreeItems.args = { + items: [ + { id: 'home', url: '#', name: 'Home' }, + { id: 'blog', url: '#', name: 'Blog' }, + { id: 'post1', url: '#', name: 'A Post' }, + ], +}; diff --git a/src/components/molecules/nav/breadcrumb.test.tsx b/src/components/molecules/nav/breadcrumb.test.tsx new file mode 100644 index 0000000..43220c9 --- /dev/null +++ b/src/components/molecules/nav/breadcrumb.test.tsx @@ -0,0 +1,15 @@ +import { render, screen } from '@test-utils'; +import Breadcrumb, { type BreadcrumbItem } from './breadcrumb'; + +const items: BreadcrumbItem[] = [ + { id: 'home', url: '#', name: 'Home' }, + { id: 'blog', url: '#', name: 'Blog' }, + { id: 'post1', url: '#', name: 'A Post' }, +]; + +describe('Breadcrumb', () => { + it('renders a navigation', () => { + render(<Breadcrumb items={items} />); + expect(screen.getByRole('navigation')).toBeInTheDocument(); + }); +}); diff --git a/src/components/molecules/nav/breadcrumb.tsx b/src/components/molecules/nav/breadcrumb.tsx new file mode 100644 index 0000000..d184d65 --- /dev/null +++ b/src/components/molecules/nav/breadcrumb.tsx @@ -0,0 +1,127 @@ +import Link from '@components/atoms/links/link'; +import { settings } from '@utils/config'; +import Script from 'next/script'; +import { FC } from 'react'; +import { useIntl } from 'react-intl'; +import { BreadcrumbList, ListItem, WithContext } from 'schema-dts'; +import styles from './breadcrumb.module.scss'; + +export type BreadcrumbItem = { + /** + * The item id. + */ + id: string; + /** + * The item URL. + */ + url: string; + /** + * The item name. + */ + name: string; +}; + +export type BreadcrumbProps = { + /** + * Set additional classnames to the nav element. + */ + className?: string; + /** + * Set additional classnames to the breadcrumb items. + */ + itemClassName?: string; + /** + * The breadcrumb items + */ + items: BreadcrumbItem[]; +}; + +/** + * Breadcrumb component + * + * Render a breadcrumb navigation. + */ +const Breadcrumb: FC<BreadcrumbProps> = ({ + itemClassName = '', + items, + ...props +}) => { + const intl = useIntl(); + + const ariaLabel = intl.formatMessage({ + defaultMessage: 'Breadcrumb', + description: 'Breadcrumb: an accessible name for the breadcrumb nav.', + id: '28nnDY', + }); + + /** + * Retrieve the breadcrumb list items. + * + * @param {BreadcrumbItem[]} list - The breadcrumb items. + * @returns {JSX.Element[]} The list items. + */ + const getListItems = (list: BreadcrumbItem[]): JSX.Element[] => { + return list.map((item, index) => { + const isLastItem = index === list.length - 1; + const itemStyles = isLastItem + ? `${styles.item} screen-reader-text` + : styles.item; + + return ( + <li key={item.id} className={`${itemStyles} ${itemClassName}`}> + {isLastItem ? item.name : <Link href={item.url}>{item.name}</Link>} + </li> + ); + }); + }; + + /** + * Retrieve the breadcrumb list items with Schema.org format. + * + * @param {BreadcrumbItem[]} list - The breadcrumb items. + * @returns {ListItem[]} An array of list items using Schema.org format. + */ + const getSchemaItems = (list: BreadcrumbItem[]): ListItem[] => { + const schemaItems: ListItem[] = []; + + list.forEach((item, index) => { + schemaItems.push({ + '@type': 'ListItem', + position: index + 1, + name: item.name, + item: item.url, + }); + }); + + return schemaItems; + }; + + const schemaJsonLd: WithContext<BreadcrumbList> = { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + '@id': `${settings.url}/#breadcrumb`, + itemListElement: getSchemaItems(items), + }; + + return ( + <> + <Script + id="schema-breadcrumb" + type="application/ld+json" + dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} + /> + <nav aria-label={ariaLabel} {...props}> + <span className="screen-reader-text"> + {intl.formatMessage({ + defaultMessage: 'You are here:', + description: 'Breadcrumb: You are here prefix', + id: '16zl9Z', + })} + </span> + <ol className={styles.list}>{getListItems(items)}</ol> + </nav> + </> + ); +}; + +export default Breadcrumb; diff --git a/src/components/molecules/nav/nav.module.scss b/src/components/molecules/nav/nav.module.scss new file mode 100644 index 0000000..9c0f6de --- /dev/null +++ b/src/components/molecules/nav/nav.module.scss @@ -0,0 +1,22 @@ +@use "@styles/abstracts/mixins" as mix; +@use "@styles/abstracts/placeholders"; + +.nav { + &__list { + @extend %reset-list; + + display: flex; + flex-flow: row wrap; + gap: var(--spacing-2xs); + align-items: center; + } + + &--footer & { + &__item:not(:first-child) { + &::before { + content: "\2022"; + margin-right: var(--spacing-2xs); + } + } + } +} diff --git a/src/components/molecules/nav/nav.stories.tsx b/src/components/molecules/nav/nav.stories.tsx new file mode 100644 index 0000000..f3a29a6 --- /dev/null +++ b/src/components/molecules/nav/nav.stories.tsx @@ -0,0 +1,107 @@ +import Envelop from '@components/atoms/icons/envelop'; +import Home from '@components/atoms/icons/home'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import NavComponent, { type NavItem } from './nav'; + +/** + * Nav - Storybook Meta + */ +export default { + title: 'Molecules/Navigation/Nav', + component: NavComponent, + argTypes: { + 'aria-label': { + control: { + type: 'text', + }, + description: 'An accessible name for the navigation.', + table: { + category: 'Accessibility', + }, + type: { + name: 'string', + required: false, + }, + }, + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the navigation wrapper.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + items: { + control: { + type: null, + }, + description: 'The nav items.', + type: { + name: 'other', + required: true, + value: '', + }, + }, + kind: { + control: { + type: 'select', + }, + description: 'The navigation kind.', + options: ['main', 'footer'], + type: { + name: 'string', + required: true, + }, + }, + listClassName: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the navigation list.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + }, +} as ComponentMeta<typeof NavComponent>; + +const Template: ComponentStory<typeof NavComponent> = (args) => ( + <NavComponent {...args} /> +); + +const MainNavItems: NavItem[] = [ + { id: 'homeLink', href: '/', label: 'Home', logo: <Home /> }, + { id: 'contactLink', href: '/contact', label: 'Contact', logo: <Envelop /> }, +]; + +const FooterNavItems: NavItem[] = [ + { id: 'contactLink', href: '/contact', label: 'Contact' }, + { id: 'legalLink', href: '/legal-notice', label: 'Legal notice' }, +]; + +/** + * Nav Stories - Main navigation + */ +export const MainNav = Template.bind({}); +MainNav.args = { + items: MainNavItems, + kind: 'main', +}; + +/** + * Nav Stories - Footer navigation + */ +export const FooterNav = Template.bind({}); +FooterNav.args = { + items: FooterNavItems, + kind: 'footer', +}; diff --git a/src/components/molecules/nav/nav.test.tsx b/src/components/molecules/nav/nav.test.tsx new file mode 100644 index 0000000..183ca0b --- /dev/null +++ b/src/components/molecules/nav/nav.test.tsx @@ -0,0 +1,28 @@ +import Envelop from '@components/atoms/icons/envelop'; +import Home from '@components/atoms/icons/home'; +import { render, screen } from '@test-utils'; +import Nav, { type NavItem } from './nav'; + +const navItems: NavItem[] = [ + { id: 'homeLink', href: '/', label: 'Home', logo: <Home /> }, + { id: 'contactLink', href: '/contact', label: 'Contact', logo: <Envelop /> }, +]; + +describe('Nav', () => { + it('renders a main navigation', () => { + render(<Nav kind="main" items={navItems} />); + expect(screen.getByRole('navigation')).toHaveClass('nav--main'); + }); + + it('renders a footer navigation', () => { + render(<Nav kind="footer" items={navItems} />); + expect(screen.getByRole('navigation')).toHaveClass('nav--footer'); + }); + + it('renders navigation links', () => { + render(<Nav kind="main" items={navItems} />); + expect( + screen.getByRole('link', { name: navItems[0].label }) + ).toHaveAttribute('href', navItems[0].href); + }); +}); diff --git a/src/components/molecules/nav/nav.tsx b/src/components/molecules/nav/nav.tsx new file mode 100644 index 0000000..581f813 --- /dev/null +++ b/src/components/molecules/nav/nav.tsx @@ -0,0 +1,85 @@ +import Link from '@components/atoms/links/link'; +import NavLink from '@components/atoms/links/nav-link'; +import { FC, ReactNode } from 'react'; +import styles from './nav.module.scss'; + +export type NavItem = { + /** + * The item id. + */ + id: string; + /** + * The item link. + */ + href: string; + /** + * The item name. + */ + label: string; + /** + * The item logo. + */ + logo?: ReactNode; +}; + +export type NavProps = { + /** + * An accessible name. + */ + 'aria-label'?: string; + /** + * Set additional classnames to the navigation wrapper. + */ + className?: string; + /** + * The navigation items. + */ + items: NavItem[]; + /** + * The navigation kind. + */ + kind: 'main' | 'footer'; + /** + * Set additional classnames to the navigation list. + */ + listClassName?: string; +}; + +/** + * Nav component + * + * Render the nav links. + */ +const Nav: FC<NavProps> = ({ + className = '', + items, + kind, + listClassName = '', + ...props +}) => { + const kindClass = `nav--${kind}`; + + /** + * Get the nav items. + * @returns {JSX.Element[]} An array of nav items. + */ + const getItems = (): JSX.Element[] => { + return items.map(({ id, href, label, logo }) => ( + <li key={id} className={styles.nav__item}> + {kind === 'main' ? ( + <NavLink href={href} label={label} logo={logo} /> + ) : ( + <Link href={href}>{label}</Link> + )} + </li> + )); + }; + + return ( + <nav className={`${styles[kindClass]} ${className}`} {...props}> + <ul className={`${styles.nav__list} ${listClassName}`}>{getItems()}</ul> + </nav> + ); +}; + +export default Nav; diff --git a/src/components/molecules/nav/pagination.module.scss b/src/components/molecules/nav/pagination.module.scss new file mode 100644 index 0000000..56c5bfc --- /dev/null +++ b/src/components/molecules/nav/pagination.module.scss @@ -0,0 +1,51 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/placeholders"; + +.wrapper { + .list { + @extend %flex-list; + + align-items: stretch; + justify-content: center; + position: relative; + row-gap: var(--spacing-xs); + column-gap: var(--spacing-sm); + + &--pages { + column-gap: var(--spacing-2xs); + margin-bottom: var(--spacing-sm); + } + } + + .link { + height: 100%; + min-width: 5ch; + min-height: 6ex; + position: relative; + + &:not(&--disabled) { + &:hover, + &:focus { + z-index: 3; + } + } + + &--number { + padding: 0; + } + + &--disabled { + display: flex; + place-content: center; + align-items: center; + background: var(--color-bg); + border: fun.convert-px(3) solid var(--color-primary-darker); + border-radius: fun.convert-px(5); + color: var(--color-primary-darker); + font-size: var(--font-size-md); + font-weight: 600; + text-decoration: underline transparent 0; + transform: scale(var(--scale-down, 0.94)); + } + } +} diff --git a/src/components/molecules/nav/pagination.stories.tsx b/src/components/molecules/nav/pagination.stories.tsx new file mode 100644 index 0000000..2e86db4 --- /dev/null +++ b/src/components/molecules/nav/pagination.stories.tsx @@ -0,0 +1,171 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import PaginationComponent from './pagination'; + +/** + * Pagination - Storybook Meta + */ +export default { + title: 'Molecules/Navigation/Pagination', + component: PaginationComponent, + args: { + baseUrl: '/page/', + siblings: 1, + }, + argTypes: { + 'aria-label': { + control: { + type: 'text', + }, + description: 'An accessible name for the pagination.', + table: { + category: 'Accessibility', + }, + type: { + name: 'string', + required: false, + }, + }, + baseUrl: { + control: { + type: 'text', + }, + description: 'The url prefix.', + table: { + category: 'Options', + defaultValue: { summary: '/page/' }, + }, + type: { + name: 'string', + required: false, + }, + }, + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the pagination wrapper.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + current: { + control: { + type: 'number', + }, + description: 'The current page number.', + type: { + name: 'number', + required: true, + }, + }, + perPage: { + control: { + type: 'number', + }, + description: 'The number of items per page.', + type: { + name: 'number', + required: true, + }, + }, + siblings: { + control: { + type: 'number', + }, + description: + 'The number of pages to show next to the current page for one side.', + table: { + category: 'Options', + defaultValue: { summary: 1 }, + }, + type: { + name: 'number', + required: false, + }, + }, + total: { + control: { + type: 'number', + }, + description: 'The total number of items.', + type: { + name: 'number', + required: true, + }, + }, + }, +} as ComponentMeta<typeof PaginationComponent>; + +const Template: ComponentStory<typeof PaginationComponent> = (args) => ( + <PaginationComponent {...args} /> +); + +/** + * Pagination Stories - Less than 5 pages + */ +export const WithoutDots = Template.bind({}); +WithoutDots.args = { + current: 2, + perPage: 10, + siblings: 2, + total: 50, +}; + +/** + * Pagination Stories - Truncated to the right. + */ +export const RightDots = Template.bind({}); +RightDots.args = { + current: 2, + perPage: 10, + siblings: 2, + total: 80, +}; + +/** + * Pagination Stories - Truncated to the left. + */ +export const LeftDots = Template.bind({}); +LeftDots.args = { + current: 7, + perPage: 10, + siblings: 2, + total: 80, +}; + +/** + * Pagination Stories - Truncated both sides. + */ +export const LeftAndRightDots = Template.bind({}); +LeftAndRightDots.args = { + current: 6, + perPage: 10, + siblings: 2, + total: 150, +}; + +/** + * Pagination Stories - Without previous link + */ +export const WithoutPreviousLink = Template.bind({}); +WithoutPreviousLink.args = { + current: 1, + perPage: 10, + siblings: 2, + total: 50, +}; + +/** + * Pagination Stories - Without next link + */ +export const WithoutNextLink = Template.bind({}); +WithoutNextLink.args = { + current: 5, + perPage: 10, + siblings: 2, + total: 50, +}; diff --git a/src/components/molecules/nav/pagination.test.tsx b/src/components/molecules/nav/pagination.test.tsx new file mode 100644 index 0000000..2c4a063 --- /dev/null +++ b/src/components/molecules/nav/pagination.test.tsx @@ -0,0 +1,26 @@ +import { render, screen } from '@test-utils'; +import Pagination from './pagination'; + +const total = 50; +const perPage = 10; + +describe('Pagination', () => { + it('renders previous and next page links', () => { + render(<Pagination current={2} total={total} perPage={perPage} />); + expect( + screen.getByRole('link', { name: /Previous page/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole('link', { name: /Next page/i }) + ).toBeInTheDocument(); + }); + + it('renders the page links except for the current one', () => { + render( + <Pagination current={2} siblings={2} total={total} perPage={perPage} /> + ); + expect(screen.getAllByRole('link', { name: /Page / })).toHaveLength( + total / perPage - 1 + ); + }); +}); diff --git a/src/components/molecules/nav/pagination.tsx b/src/components/molecules/nav/pagination.tsx new file mode 100644 index 0000000..934b50a --- /dev/null +++ b/src/components/molecules/nav/pagination.tsx @@ -0,0 +1,220 @@ +import ButtonLink from '@components/atoms/buttons/button-link'; +import { FC, Fragment, ReactNode } from 'react'; +import { useIntl } from 'react-intl'; +import styles from './pagination.module.scss'; + +export type PaginationProps = { + /** + * An accessible name for the pagination. + */ + 'aria-label'?: string; + /** + * The url part before page number. Default: /page/ + */ + baseUrl?: string; + /** + * Set additional classnames to the pagination wrapper. + */ + className?: string; + /** + * The current page number. + */ + current: number; + /** + * The number of items per page. + */ + perPage: number; + /** + * The number of siblings on one side of the current page. Default: 1. + */ + siblings?: number; + /** + * The total number of items. + */ + total: number; +}; + +/** + * Pagination component + * + * Render a page-based navigation. + */ +const Pagination: FC<PaginationProps> = ({ + baseUrl = '/page/', + className = '', + current, + perPage, + siblings = 2, + total, + ...props +}) => { + const intl = useIntl(); + const totalPages = Math.round(total / perPage); + const hasPreviousPage = current > 1; + const previousPageName = intl.formatMessage( + { + defaultMessage: '{icon} Previous page', + description: 'Pagination: previous page link', + id: 'aMFqPH', + }, + { icon: '←' } + ); + const previousPageUrl = `${baseUrl}${current - 1}`; + const hasNextPage = current < totalPages; + const nextPageName = intl.formatMessage( + { + defaultMessage: 'Next page {icon}', + description: 'Pagination: Next page link', + id: 'R4yaW6', + }, + { icon: '→' } + ); + const nextPageUrl = `${baseUrl}${current + 1}`; + + /** + * Create an array with a range of values from start value to end value. + * + * @param {number} start - The first value. + * @param {number} end - The last value. + * @returns {number[]} An array from start value to end value. + */ + const range = (start: number, end: number): number[] => { + const length = end - start + 1; + + return Array.from({ length }, (_, index) => index + start); + }; + + /** + * Get the pagination range. + * + * @param currentPage - The current page number. + * @param maxPages - The total pages number. + * @returns {(number|string)[]} An array of page numbers with or without dots. + */ + const getPaginationRange = ( + currentPage: number, + maxPages: number + ): (number | string)[] => { + const dots = '\u2026'; + + /** + * Show left dots if current page less left siblings is greater than the + * first two pages. + */ + const hasLeftDots = currentPage - siblings > 2; + + /** + * Show right dots if current page plus right siblings is lower than the + * total of pages less the last page. + */ + const hasRightDots = currentPage + siblings < maxPages - 1; + + if (hasLeftDots && hasRightDots) { + const middleItems = range(currentPage - siblings, currentPage + siblings); + return [1, dots, ...middleItems, dots, maxPages]; + } + + if (hasLeftDots) { + const rightItems = range(currentPage - siblings, maxPages); + return [1, dots, ...rightItems]; + } + + if (hasRightDots) { + const leftItems = range(1, currentPage + siblings); + return [...leftItems, dots, maxPages]; + } + + return range(1, maxPages); + }; + + /** + * Get a link or a span wrapped in a list item. + * + * @param {string} id - The item id. + * @param {ReactNode} body - The link body. + * @param {string} [link] - An URL. + * @returns {JSX.Element} The list item. + */ + const getItem = (id: string, body: ReactNode, link?: string): JSX.Element => { + const linkModifier = id.startsWith('page') ? 'link--number' : ''; + const kind = id === 'previous' || id === 'next' ? 'tertiary' : 'secondary'; + + return ( + <li className={styles.item}> + {link ? ( + <ButtonLink + kind={kind} + target={link} + className={`${styles.link} ${styles[linkModifier]}`} + > + {body} + </ButtonLink> + ) : ( + <span className={`${styles.link} ${styles['link--disabled']}`}> + {body} + </span> + )} + </li> + ); + }; + + /** + * Get the list of pages. + * + * @param {number} currentPage - The current page number. + * @param {number} maxPages - The total of pages. + * @returns {JSX.Element[]} The list items. + */ + const getPages = (currentPage: number, maxPages: number): JSX.Element[] => { + const pagesRange = getPaginationRange(currentPage, maxPages); + + return pagesRange.map((page, index) => { + const id = typeof page === 'string' ? `dots-${index}` : `page-${page}`; + const currentPagePrefix = intl.formatMessage({ + defaultMessage: 'You are here:', + description: 'Pagination: current page indication', + id: 'yE/Jdz', + }); + const body = + typeof page === 'string' + ? '\u2026' + : intl.formatMessage( + { + defaultMessage: '<a11y>Page </a11y>{number}', + description: 'Pagination: page number', + id: 'TSXPzr', + }, + { + number: page, + a11y: (chunks: ReactNode) => ( + <span className="screen-reader-text"> + {page === currentPage && currentPagePrefix} + {chunks} + </span> + ), + } + ); + const url = + page === currentPage || typeof page === 'string' + ? undefined + : `${baseUrl}${page}`; + + return <Fragment key={`item-${id}`}>{getItem(id, body, url)}</Fragment>; + }); + }; + + return ( + <nav className={`${styles.wrapper} ${className}`} {...props}> + <ul className={`${styles.list} ${styles['list--pages']}`}> + {getPages(current, totalPages)} + </ul> + <ul className={styles.list}> + {hasPreviousPage && + getItem('previous', previousPageName, previousPageUrl)} + {hasNextPage && getItem('next', nextPageName, nextPageUrl)} + </ul> + </nav> + ); +}; + +export default Pagination; |
