diff --git a/docs/upgrade-to-6.0.md b/docs/upgrade-to-6.0.md index ddc790a12..f780a654c 100644 --- a/docs/upgrade-to-6.0.md +++ b/docs/upgrade-to-6.0.md @@ -22,19 +22,20 @@ The updated [header](https://service-manual.nhs.uk/design-system/components/head You can now use smaller versions of the [radios](https://service-manual.nhs.uk/design-system/components/radios) and [checkboxes](https://service-manual.nhs.uk/design-system/components/checkboxes) components by adding the `small` prop. -### Panel component +### Numbered pagination component -The [panel](https://service-manual.nhs.uk/design-system/components/panel) component from NHS.UK frontend v9.3.0 has been added: +The [pagination](https://service-manual.nhs.uk/design-system/components/notification-banner) component from NHS.UK frontend v10.1 has been updated to support numbered pagination: ```jsx - - Booking complete - We have sent you a confirmation email - + + + + + + + ``` -This replaces the [list panel component](#list-panel) which was removed in NHS.UK frontend v6.0.0. - ### Notification banner component The [notification banner](https://service-manual.nhs.uk/design-system/components/notification-banner) component from NHS.UK frontend v10 has been added: @@ -46,23 +47,32 @@ The [notification banner](https://service-manual.nhs.uk/design-system/components ``` +### Panel component + +The [panel](https://service-manual.nhs.uk/design-system/components/panel) component from NHS.UK frontend v9.3.0 has been added: + +```jsx + + Booking complete + We have sent you a confirmation email + +``` + +This replaces the [list panel component](#list-panel) which was removed in NHS.UK frontend v6.0.0. + ### Support for React Server Components (RSC) All components have been tested as React Server Components (RSC) but due to [multipart namespace component limitations](https://ivicabatinic.from.hr/posts/multipart-namespace-components-addressing-rsc-and-dot-notation-issues) an alternative syntax (without dot notation) can be used as a workaround: ```patch - -- -+ - Treatments -- -+ -- -+ - Symptoms -- -+ - + +- Home +- NHS services +- Hospitals ++ Home ++ NHS services ++ Hospitals + ``` ## Breaking changes @@ -441,6 +451,26 @@ To align with NHS.UK frontend, the error summary component is automatically aler ``` +### Pagination + +To align with NHS.UK frontend, the pagination link component automatically renders its own "Previous page" or "Next page" text, with "page" being visually hidden. You will need to make the following changes: + +- rename the `Pagination.Link` component to `Pagination.Item` +- move text content (or the `children` prop) to the `labelText` prop + +```patch + +- +- Treatments +- +- +- Symptoms +- ++ ++ + +``` + ### Select You must rename the `Select` prop `selectRef` to `ref` for consistency with other components: diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 5eab14f0b..5755e98d2 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -96,7 +96,9 @@ describe('Index', () => { 'NotificationBannerLink', 'NotificationBannerTitle', 'Pagination', + 'PaginationItem', 'PaginationLink', + 'PaginationLinkText', 'Panel', 'PanelTitle', 'Radios', diff --git a/src/components/navigation/pagination/Pagination.tsx b/src/components/navigation/pagination/Pagination.tsx index a202b432a..f7d01cff0 100644 --- a/src/components/navigation/pagination/Pagination.tsx +++ b/src/components/navigation/pagination/Pagination.tsx @@ -1,63 +1,54 @@ import classNames from 'classnames'; -import { forwardRef, type ComponentPropsWithoutRef } from 'react'; -import { ArrowLeftIcon, ArrowRightIcon } from '#components/content-presentation/index.js'; -import { type AsElementLink } from '#util/types/LinkTypes.js'; +import { Children, forwardRef, type ComponentPropsWithoutRef } from 'react'; +import { PaginationItem, PaginationLink } from './components/index.js'; +import { childIsOfComponentType } from '#util/types/TypeGuards.js'; -export interface PaginationLinkProps extends AsElementLink { - previous?: boolean; - next?: boolean; -} +export type PaginationProps = ComponentPropsWithoutRef<'nav'>; + +const PaginationComponent = forwardRef( + ({ className, children, 'aria-label': ariaLabel = 'Pagination', ...rest }, forwardedRef) => { + const items = Children.toArray(children); + + // Filter previous and next links + const links = items.filter((child) => childIsOfComponentType(child, PaginationLink)); + const linkPrevious = links.find(({ props }) => props.previous); + const linkNext = links.find(({ props }) => props.next); + + // Filter numbered list items + const listItems = items.filter((child) => childIsOfComponentType(child, PaginationItem)); + const listItemsNumbered = listItems.filter(({ props }) => props.number || props.ellipsis); -export const PaginationLink = forwardRef( - ({ className, children, asElement: Element = 'a', previous, next, ...rest }, forwardedRef) => ( -
  • - - - {previous ? 'Previous' : null} - {next ? 'Next' : null} - - : - {children} - {previous ? : null} - {next ? : null} - -
  • - ), -); - -export type PaginationProps = ComponentPropsWithoutRef<'nav'>; - -const PaginationComponent = forwardRef( - ({ className, children, 'aria-label': ariaLabel = 'Pagination', ...rest }, forwardedRef) => ( - - ), + {linkPrevious} +
      + {listItems} +
    + {linkNext} + + ); + }, ); PaginationComponent.displayName = 'Pagination'; -PaginationLink.displayName = 'Pagination.Link'; export const Pagination = Object.assign(PaginationComponent, { + Item: PaginationItem, Link: PaginationLink, }); diff --git a/src/components/navigation/pagination/__tests__/Pagination.test.tsx b/src/components/navigation/pagination/__tests__/Pagination.test.tsx index a435ffd7d..582ce2081 100644 --- a/src/components/navigation/pagination/__tests__/Pagination.test.tsx +++ b/src/components/navigation/pagination/__tests__/Pagination.test.tsx @@ -1,5 +1,6 @@ import { render } from '@testing-library/react'; import { Pagination } from '..'; +import { createRef } from 'react'; describe('Pagination', () => { it('matches snapshot', () => { @@ -8,45 +9,267 @@ describe('Pagination', () => { expect(container).toMatchSnapshot('Pagination'); }); + it('matches snapshot with next item', () => { + const { container } = render( + + + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot with previous item', () => { + const { container } = render( + + + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot with previous and next items', () => { + const { container } = render( + + + + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot with previous and next items (translated)', () => { + const { container } = render( + + + Blaenorol + + + Nesaf + + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot with numbered items', () => { + const { container } = render( + + + + + + + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot with numbered items (translated)', () => { + const { container } = render( + + + Blaenorol + + + + + + Nesaf + + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('forwards refs', () => { + const ref = createRef(); + + const { container } = render( + + + + , + ); + + const paginationEl = container.querySelector('nav'); + + expect(ref.current).toBe(paginationEl); + expect(ref.current).toHaveClass('nhsuk-pagination'); + }); + + it('forwards refs with numbered items', () => { + const ref = createRef(); + + const { container } = render( + + + + + + + , + ); + + const paginationEl = container.querySelector('nav'); + + expect(ref.current).toBe(paginationEl); + expect(ref.current).toHaveClass('nhsuk-pagination', 'nhsuk-pagination--numbered'); + }); + + describe('Pagination.Item', () => { + it('matches snapshot with previous item', () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot with previous item (no label text)', () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot with next item', () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot with next item (no label text)', () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot with number', () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot with ellipsis', () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); + + it('renders previous elements', () => { + const { container } = render(); + + const itemEl = container.querySelector('li'); + const linkEl = container.querySelector('a'); + const titleEl = container.querySelector('.nhsuk-pagination__title'); + const pageEl = container.querySelector('.nhsuk-pagination__page'); + const iconEl = container.querySelector('.nhsuk-icon'); + + expect(itemEl).toHaveClass('nhsuk-pagination-item--previous'); + expect(itemEl).not.toHaveClass('nhsuk-pagination-item--next'); + + expect(linkEl).toHaveClass('nhsuk-pagination__link', 'nhsuk-pagination__link--prev'); + expect(linkEl).not.toHaveClass('nhsuk-pagination__link--next'); + + expect(titleEl).toHaveTextContent('Previous'); + expect(pageEl).toHaveTextContent('Page name'); + + expect(iconEl).toHaveClass('nhsuk-icon--arrow-left'); + expect(iconEl).not.toHaveClass('nhsuk-icon--arrow-right'); + }); + + it('renders next elements', () => { + const { container } = render(); + + const itemEl = container.querySelector('li'); + const linkEl = container.querySelector('a'); + const titleEl = container.querySelector('.nhsuk-pagination__title'); + const pageEl = container.querySelector('.nhsuk-pagination__page'); + const iconEl = container.querySelector('.nhsuk-icon'); + + expect(itemEl).not.toHaveClass('nhsuk-pagination-item--previous'); + expect(itemEl).toHaveClass('nhsuk-pagination-item--next'); + + expect(linkEl).not.toHaveClass('nhsuk-pagination__link--prev'); + expect(linkEl).toHaveClass('nhsuk-pagination__link', 'nhsuk-pagination__link--next'); + + expect(titleEl).toHaveTextContent('Next'); + expect(pageEl).toHaveTextContent('Page name'); + + expect(iconEl).not.toHaveClass('nhsuk-icon--arrow-left'); + expect(iconEl).toHaveClass('nhsuk-icon--arrow-right'); + }); + + it('renders number elements', () => { + const { container } = render(); + + const itemEl = container.querySelector('li'); + const linkEl = container.querySelector('a'); + + expect(itemEl).toHaveClass('nhsuk-pagination__item'); + expect(linkEl).toHaveClass('nhsuk-pagination__link'); + expect(linkEl).toHaveTextContent('10'); + expect(linkEl).toHaveAccessibleName('Page 10'); + }); + + it('renders ellipsis elements', () => { + const { container } = render(); + + const itemEl = container.querySelector('li'); + const linkEl = container.querySelector('a'); + + expect(itemEl).toHaveClass('nhsuk-pagination__item', 'nhsuk-pagination__item--ellipsis'); + expect(itemEl).toHaveTextContent('⋯'); + expect(linkEl).toBeNull(); + }); + }); + describe('Pagination.Link', () => { - it('matches snapshot', () => { - const { container } = render(); + it('matches snapshot with previous link', () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot with next link', () => { + const { container } = render(); - expect(container).toMatchSnapshot('Pagination.Link'); + expect(container).toMatchSnapshot(); }); it('renders previous elements', () => { - const { container } = render(PreviousText); - - expect(container.querySelector('.nhsuk-pagination-item--previous')).toBeTruthy(); - expect(container.querySelector('.nhsuk-pagination-item--next')).toBeFalsy(); - expect(container.querySelector('.nhsuk-pagination__title')?.textContent).toBe('Previous'); - expect(container.querySelector('.nhsuk-icon--arrow-left')).toBeTruthy(); - expect(container.querySelector('.nhsuk-icon--arrow-right')).toBeFalsy(); - expect(container.querySelector('.nhsuk-pagination__page')?.textContent).toBe('PreviousText'); - expect( - container.querySelector('.nhsuk-pagination__link.nhsuk-pagination__link--prev'), - ).toBeTruthy(); - expect( - container.querySelector('.nhsuk-pagination__link.nhsuk-pagination__link--next'), - ).toBeFalsy(); + const { container } = render(); + + const linkEl = container.querySelector('a'); + const titleEl = container.querySelector('.nhsuk-pagination__title'); + const iconEl = container.querySelector('.nhsuk-icon'); + + expect(linkEl).toHaveClass('nhsuk-pagination__previous'); + expect(linkEl).not.toHaveClass('nhsuk-pagination__next'); + + expect(titleEl).toHaveTextContent('Previous'); + + expect(iconEl).toHaveClass('nhsuk-icon--arrow-left'); + expect(iconEl).not.toHaveClass('nhsuk-icon--arrow-right'); }); it('renders next elements', () => { - const { container } = render(NextText); - - expect(container.querySelector('.nhsuk-pagination-item--previous')).toBeFalsy(); - expect(container.querySelector('.nhsuk-pagination-item--next')).toBeTruthy(); - expect(container.querySelector('.nhsuk-pagination__title')?.textContent).toBe('Next'); - expect(container.querySelector('.nhsuk-icon--arrow-left')).toBeFalsy(); - expect(container.querySelector('.nhsuk-icon--arrow-right')).toBeTruthy(); - expect(container.querySelector('.nhsuk-pagination__page')?.textContent).toBe('NextText'); - expect( - container.querySelector('.nhsuk-pagination__link.nhsuk-pagination__link--prev'), - ).toBeFalsy(); - expect( - container.querySelector('.nhsuk-pagination__link.nhsuk-pagination__link--next'), - ).toBeTruthy(); + const { container } = render(); + + const linkEl = container.querySelector('a'); + const titleEl = container.querySelector('.nhsuk-pagination__title'); + const iconEl = container.querySelector('.nhsuk-icon'); + + expect(linkEl).not.toHaveClass('nhsuk-pagination__previous'); + expect(linkEl).toHaveClass('nhsuk-pagination__next'); + + expect(titleEl).toHaveTextContent('Next'); + + expect(iconEl).not.toHaveClass('nhsuk-icon--arrow-left'); + expect(iconEl).toHaveClass('nhsuk-icon--arrow-right'); }); }); }); diff --git a/src/components/navigation/pagination/__tests__/__snapshots__/Pagination.test.tsx.snap b/src/components/navigation/pagination/__tests__/__snapshots__/Pagination.test.tsx.snap index 6bc03fa1c..9bc515934 100644 --- a/src/components/navigation/pagination/__tests__/__snapshots__/Pagination.test.tsx.snap +++ b/src/components/navigation/pagination/__tests__/__snapshots__/Pagination.test.tsx.snap @@ -1,16 +1,175 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`Pagination Pagination.Link matches snapshot: Pagination.Link 1`] = ` +exports[`Pagination Pagination.Item matches snapshot with ellipsis 1`] = `
  • + ⋯ +
  • +
    +`; + +exports[`Pagination Pagination.Item matches snapshot with next item (no label text) 1`] = ` + +`; + +exports[`Pagination Pagination.Item matches snapshot with next item 1`] = ` + +`; + +exports[`Pagination Pagination.Item matches snapshot with number 1`] = ` +
    +
  • + + 10 + +
  • +
    +`; + +exports[`Pagination Pagination.Item matches snapshot with previous item (no label text) 1`] = ` + +`; + +exports[`Pagination Pagination.Item matches snapshot with previous item 1`] = ` + `; +exports[`Pagination Pagination.Link matches snapshot with next link 1`] = ` + +`; + +exports[`Pagination Pagination.Link matches snapshot with previous link 1`] = ` + +`; + +exports[`Pagination matches snapshot with next item 1`] = ` + +`; + +exports[`Pagination matches snapshot with numbered items (translated) 1`] = ` + +`; + +exports[`Pagination matches snapshot with numbered items 1`] = ` + +`; + +exports[`Pagination matches snapshot with previous and next items (translated) 1`] = ` + +`; + +exports[`Pagination matches snapshot with previous and next items 1`] = ` + +`; + +exports[`Pagination matches snapshot with previous item 1`] = ` + +`; + exports[`Pagination matches snapshot: Pagination 1`] = `