├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── ci.yml │ ├── release.yml │ └── storybook.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── .storybook ├── main.ts ├── manager.ts ├── preview-head.html ├── preview.ts ├── storybook.scss └── theme.ts ├── .vscode ├── launch.json └── settings.json ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bundle-base.tsconfig.json ├── docs ├── upgrade-to-1.0.md ├── upgrade-to-2.0.md ├── upgrade-to-3.0.md ├── upgrade-to-4.0.md └── upgrade-to-5.0.md ├── jest.config.js ├── package.json ├── rollup.config.mjs ├── src ├── __mocks__ │ └── styleMock.ts ├── __tests__ │ └── index.test.ts ├── components │ ├── content-presentation │ │ ├── details │ │ │ ├── Details.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Details.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── Details.test.tsx.snap │ │ │ └── index.ts │ │ ├── do-and-dont-list │ │ │ ├── DoAndDontList.tsx │ │ │ ├── __tests__ │ │ │ │ ├── DoAndDontList.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── DoAndDontList.test.tsx.snap │ │ │ └── index.ts │ │ ├── hero │ │ │ ├── Hero.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Hero.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── Hero.test.tsx.snap │ │ │ └── index.ts │ │ ├── icons │ │ │ ├── BaseIcon.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Icons.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── Icons.test.tsx.snap │ │ │ ├── index.ts │ │ │ └── individual │ │ │ │ ├── ArrowLeft.tsx │ │ │ │ ├── ArrowRight.tsx │ │ │ │ ├── ArrowRightCircle.tsx │ │ │ │ ├── ChevronDown.tsx │ │ │ │ ├── ChevronLeft.tsx │ │ │ │ ├── ChevronRight.tsx │ │ │ │ ├── ChevronRightCircle.tsx │ │ │ │ ├── Close.tsx │ │ │ │ ├── Cross.tsx │ │ │ │ ├── Emdash.tsx │ │ │ │ ├── Minus.tsx │ │ │ │ ├── Plus.tsx │ │ │ │ ├── Search.tsx │ │ │ │ ├── SmallEmdash.tsx │ │ │ │ └── Tick.tsx │ │ ├── images │ │ │ ├── Images.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Images.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── Images.test.tsx.snap │ │ │ └── index.ts │ │ ├── inset-text │ │ │ ├── InsetText.tsx │ │ │ ├── __tests__ │ │ │ │ ├── InsetText.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── InsetText.test.tsx.snap │ │ │ └── index.ts │ │ ├── summary-list │ │ │ ├── SummaryList.tsx │ │ │ ├── __tests__ │ │ │ │ ├── SummaryList.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── SummaryList.test.tsx.snap │ │ │ └── index.ts │ │ ├── table │ │ │ ├── Table.tsx │ │ │ ├── TableContext.ts │ │ │ ├── TableHelpers.ts │ │ │ ├── TableSectionContext.ts │ │ │ ├── components │ │ │ │ ├── TableBody.tsx │ │ │ │ ├── TableCaption.tsx │ │ │ │ ├── TableCell.tsx │ │ │ │ ├── TableContainer.tsx │ │ │ │ ├── TableHead.tsx │ │ │ │ ├── TablePanel.tsx │ │ │ │ ├── TableRow.tsx │ │ │ │ └── __tests__ │ │ │ │ │ ├── TableBody.test.tsx │ │ │ │ │ ├── TableCaption.test.tsx │ │ │ │ │ ├── TableCell.test.tsx │ │ │ │ │ ├── TableContainer.test.tsx │ │ │ │ │ ├── TableHead.test.tsx │ │ │ │ │ ├── TablePanel.test.tsx │ │ │ │ │ ├── TableRow.test.tsx │ │ │ │ │ └── __snapshots__ │ │ │ │ │ ├── TableBody.test.tsx.snap │ │ │ │ │ ├── TableCaption.test.tsx.snap │ │ │ │ │ ├── TableCell.test.tsx.snap │ │ │ │ │ ├── TableContainer.test.tsx.snap │ │ │ │ │ ├── TableHead.test.tsx.snap │ │ │ │ │ ├── TablePanel.test.tsx.snap │ │ │ │ │ └── TableRow.test.tsx.snap │ │ │ └── index.ts │ │ ├── tabs │ │ │ ├── Tabs.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Tabs.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── Tabs.test.tsx.snap │ │ │ └── index.ts │ │ ├── tag │ │ │ ├── Tag.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Tag.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── Tag.test.tsx.snap │ │ │ └── index.ts │ │ └── warning-callout │ │ │ ├── WarningCallout.tsx │ │ │ ├── __tests__ │ │ │ ├── WarningCallout.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── WarningCallout.test.tsx.snap │ │ │ └── index.ts │ ├── form-elements │ │ ├── button │ │ │ ├── Button.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Button.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── Button.test.tsx.snap │ │ │ └── index.ts │ │ ├── character-count │ │ │ ├── CharacterCount.tsx │ │ │ ├── __tests__ │ │ │ │ ├── CharacterCount.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── CharacterCount.test.tsx.snap │ │ │ └── index.ts │ │ ├── checkboxes │ │ │ ├── CheckboxContext.ts │ │ │ ├── Checkboxes.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Checkboxes.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── Checkboxes.test.tsx.snap │ │ │ ├── components │ │ │ │ ├── Box.tsx │ │ │ │ └── Divider.tsx │ │ │ └── index.ts │ │ ├── date-input │ │ │ ├── DateInput.tsx │ │ │ ├── DateInputContext.ts │ │ │ ├── __tests__ │ │ │ │ ├── DateInput.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── DateInput.test.tsx.snap │ │ │ ├── components │ │ │ │ └── IndividualDateInputs.tsx │ │ │ └── index.ts │ │ ├── error-message │ │ │ ├── ErrorMessage.tsx │ │ │ ├── __tests__ │ │ │ │ ├── ErrorMessage.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── ErrorMessage.test.tsx.snap │ │ │ └── index.ts │ │ ├── error-summary │ │ │ ├── ErrorSummary.tsx │ │ │ ├── __tests__ │ │ │ │ ├── ErrorSummary.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── ErrorSummary.test.tsx.snap │ │ │ └── index.ts │ │ ├── fieldset │ │ │ ├── Fieldset.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Fieldset.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── Fieldset.test.tsx.snap │ │ │ └── index.ts │ │ ├── form │ │ │ ├── Form.tsx │ │ │ ├── FormContext.ts │ │ │ └── index.ts │ │ ├── hint-text │ │ │ ├── HintText.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Hint.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── Hint.test.tsx.snap │ │ │ └── index.ts │ │ ├── label │ │ │ ├── Label.tsx │ │ │ ├── __tests__ │ │ │ │ └── Label.test.tsx │ │ │ └── index.ts │ │ ├── radios │ │ │ ├── RadioContext.ts │ │ │ ├── Radios.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Radios.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── Radios.test.tsx.snap │ │ │ ├── components │ │ │ │ ├── Divider.tsx │ │ │ │ └── Radio.tsx │ │ │ └── index.ts │ │ ├── select │ │ │ ├── Select.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Select.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── Select.test.tsx.snap │ │ │ └── index.ts │ │ ├── text-input │ │ │ ├── TextInput.tsx │ │ │ ├── __tests__ │ │ │ │ └── TextInput.test.tsx │ │ │ └── index.ts │ │ └── textarea │ │ │ ├── Textarea.tsx │ │ │ ├── __tests__ │ │ │ └── Textarea.test.tsx │ │ │ └── index.ts │ ├── layout │ │ ├── Col.tsx │ │ ├── Container.tsx │ │ ├── Row.tsx │ │ ├── __tests__ │ │ │ ├── Col.test.tsx │ │ │ ├── Container.test.tsx │ │ │ ├── Row.test.tsx │ │ │ └── __snapshots__ │ │ │ │ ├── Col.test.tsx.snap │ │ │ │ ├── Container.test.tsx.snap │ │ │ │ └── Row.test.tsx.snap │ │ └── index.ts │ ├── navigation │ │ ├── action-link │ │ │ ├── ActionLink.tsx │ │ │ ├── README.md │ │ │ ├── __tests__ │ │ │ │ ├── ActionLink.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── ActionLink.test.tsx.snap │ │ │ └── index.ts │ │ ├── back-link │ │ │ ├── BackLink.tsx │ │ │ ├── README.md │ │ │ ├── __tests__ │ │ │ │ ├── BackLink.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── BackLink.test.tsx.snap │ │ │ └── index.ts │ │ ├── breadcrumb │ │ │ ├── Breadcrumb.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Breadcrumb.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── Breadcrumb.test.tsx.snap │ │ │ └── index.ts │ │ ├── card │ │ │ ├── Card.tsx │ │ │ ├── CardContext.ts │ │ │ ├── __tests__ │ │ │ │ ├── Card.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── Card.test.tsx.snap │ │ │ ├── components │ │ │ │ ├── CardContent.tsx │ │ │ │ ├── CardDescription.tsx │ │ │ │ ├── CardGroup.tsx │ │ │ │ ├── CardGroupItem.tsx │ │ │ │ ├── CardHeading.tsx │ │ │ │ ├── CardImage.tsx │ │ │ │ └── CardLink.tsx │ │ │ └── index.ts │ │ ├── contents-list │ │ │ ├── ContentsList.tsx │ │ │ ├── __tests__ │ │ │ │ ├── ContentsList.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── ContentsList.test.tsx.snap │ │ │ └── index.ts │ │ ├── footer │ │ │ ├── Footer.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Footer.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── Footer.test.tsx.snap │ │ │ └── index.ts │ │ ├── header │ │ │ ├── Header.tsx │ │ │ ├── HeaderContext.ts │ │ │ ├── __tests__ │ │ │ │ ├── Header.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── Header.test.tsx.snap │ │ │ ├── components │ │ │ │ ├── Content.tsx │ │ │ │ ├── NHSLogo.tsx │ │ │ │ ├── Nav.tsx │ │ │ │ ├── NavDropdownMenu.tsx │ │ │ │ ├── NavItem.tsx │ │ │ │ ├── OrganisationalLogo.tsx │ │ │ │ ├── Search.tsx │ │ │ │ └── TransactionalServiceName.tsx │ │ │ └── index.ts │ │ ├── pagination │ │ │ ├── Pagination.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Pagination.test.tsx │ │ │ │ └── __snapshots__ │ │ │ │ │ └── Pagination.test.tsx.snap │ │ │ └── index.ts │ │ └── skip-link │ │ │ ├── SkipLink.tsx │ │ │ ├── __tests__ │ │ │ ├── SkipLink.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── SkipLink.test.tsx.snap │ │ │ └── index.ts │ ├── typography │ │ ├── BodyText.tsx │ │ ├── LedeText.tsx │ │ ├── __tests__ │ │ │ ├── BodyText.test.tsx │ │ │ ├── LedeText.test.tsx │ │ │ └── __snapshots__ │ │ │ │ ├── BodyText.test.tsx.snap │ │ │ │ └── LedeText.test.tsx.snap │ │ └── index.ts │ └── utils │ │ ├── Clearfix.tsx │ │ ├── FormGroup.tsx │ │ ├── FormGroupContext.ts │ │ ├── HeadingLevel.tsx │ │ ├── LabelBlock.tsx │ │ ├── ReadingWidth.tsx │ │ ├── SingleInputFormGroup.tsx │ │ └── __tests__ │ │ ├── FormGroup.test.tsx │ │ ├── HeadingLevel.test.tsx │ │ ├── SingleInputFormGroup.test.tsx │ │ └── __snapshots__ │ │ ├── FormGroup.test.tsx.snap │ │ └── SingleInputFormGroup.test.tsx.snap ├── index.ts ├── patterns │ ├── nav-a-z │ │ ├── NavAZ.tsx │ │ ├── __tests__ │ │ │ ├── NavAZ.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── NavAZ.test.tsx.snap │ │ └── index.ts │ ├── panel │ │ ├── Panel.tsx │ │ ├── __tests__ │ │ │ ├── Panel.test.tsx │ │ │ └── __snapshots__ │ │ │ │ └── Panel.test.tsx.snap │ │ └── index.ts │ └── review-date │ │ ├── ReviewDate.tsx │ │ ├── __tests__ │ │ ├── ReviewDate.test.tsx │ │ └── __snapshots__ │ │ │ └── ReviewDate.test.tsx.snap │ │ └── index.ts ├── resources │ ├── character-count.js │ ├── checkboxes.js │ ├── common.js │ ├── header.js │ └── tabs.js ├── setupTests.ts └── util │ ├── IsDev.ts │ ├── RandomID.ts │ ├── __tests__ │ └── IsDev.test.ts │ ├── hooks │ └── UseDevWarning.tsx │ └── types │ ├── FormTypes.ts │ ├── LinkTypes.ts │ ├── NHSUKTypes.ts │ └── TypeGuards.ts ├── stories ├── Content Presentation │ ├── Details.stories.tsx │ ├── DoAndDontList.stories.tsx │ ├── Hero.stories.tsx │ ├── Icons.stories.tsx │ ├── Images.stories.tsx │ ├── InsetText.stories.tsx │ ├── SummaryList.stories.tsx │ ├── Table.stories.tsx │ ├── Tabs.stories.tsx │ ├── Tag.stories.tsx │ └── WarningCallout.stories.tsx ├── Form Elements │ ├── Button.stories.tsx │ ├── CharacterCount.stories.tsx │ ├── Checkboxes.stories.tsx │ ├── DateInput.stories.tsx │ ├── ErrorMessage.stories.tsx │ ├── ErrorSummary.stories.tsx │ ├── Fieldset.stories.tsx │ ├── HintText.stories.tsx │ ├── Label.stories.tsx │ ├── Radios.stories.tsx │ ├── Select.stories.tsx │ ├── TextInput.stories.tsx │ └── Textarea.stories.tsx ├── Layout │ └── Grid.stories.tsx ├── Migration Guides │ ├── UpgradeTov1.mdx │ ├── UpgradeTov2.mdx │ ├── UpgradeTov3.mdx │ └── UpgradeTov4.mdx ├── Navigation │ ├── ActionLink.stories.tsx │ ├── BackLink.stories.tsx │ ├── Breadcrumb.stories.tsx │ ├── Card.stories.tsx │ ├── ContentsList.stories.tsx │ ├── Footer.stories.tsx │ ├── Header.stories.tsx │ ├── Pagination.stories.tsx │ └── SkipLink.stories.tsx ├── Patterns │ ├── NavAZ.stories.tsx │ ├── PageAZ.stories.tsx │ ├── Panel.stories.tsx │ └── ReviewDate.stories.tsx ├── Utils │ └── FormGroup.stories.tsx └── Welcome.mdx ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | jest.config.js 2 | .eslintrc.js 3 | rollup.config.mjs 4 | dist -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | project: './tsconfig.json', 4 | tsconfigRootDir: __dirname, 5 | }, 6 | env: { 7 | browser: true, 8 | jest: true, 9 | }, 10 | settings: { 11 | 'import/resolver': { 12 | typescript: {}, 13 | }, 14 | react: { 15 | version: 'detect', 16 | }, 17 | }, 18 | extends: [ 19 | 'plugin:react/recommended', 20 | 'plugin:@typescript-eslint/recommended', 21 | 'prettier', 22 | 'plugin:import/errors', 23 | 'plugin:import/warnings', 24 | 'plugin:import/typescript', 25 | 'plugin:jsx-a11y/recommended', 26 | 'plugin:react-hooks/recommended', 27 | ], 28 | rules: { 29 | 'react/prop-types': 0, 30 | 'jsx-a11y/anchor-has-content': 0, 31 | 'jsx-a11y/alt-text': 0, 32 | 'jsx-a11y/heading-has-content': 0, 33 | 'react-hooks/exhaustive-deps': 0, 34 | }, 35 | overrides: [ 36 | { 37 | files: ['*.stories.tsx'], 38 | rules: { '@typescript-eslint/no-unused-vars': 'off' }, 39 | }, 40 | ], 41 | }; 42 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI Build 2 | 3 | on: 4 | push: 5 | branches: main 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up Node.js 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: '18.x' 18 | - name: Enable corepack 19 | run: corepack enable 20 | - name: Set Yarn version 21 | run: yarn set version stable 22 | - name: Yarn Install 23 | run: yarn 24 | - name: Lint 25 | run: yarn lint:ci 26 | - name: Jest Tests 27 | run: yarn test:ci 28 | - name: Typescript build 29 | run: yarn build 30 | - name: Storybook build 31 | run: yarn build-storybook 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: CD Build and Publish to NPM 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Set up Node.js 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: '18.x' 20 | 21 | - name: Enable corepack 22 | run: corepack enable 23 | 24 | - name: Set Yarn version 25 | run: yarn set version stable 26 | 27 | - name: Yarn Install 28 | run: yarn 29 | 30 | - name: Lint 31 | run: yarn lint:ci 32 | 33 | - name: Jest Tests 34 | run: yarn test:ci 35 | 36 | - name: Typescript build 37 | run: yarn build 38 | 39 | - name: Set Version to Release Tag Name 40 | run: | 41 | yarn version ${{ github.event.release.tag_name }} 42 | 43 | - name: Publish to npm, publish pre-release as beta 44 | uses: JS-DevTools/npm-publish@v1 45 | with: 46 | token: ${{ secrets.NPM_TOKEN }} 47 | tag: ${{ github.event.release.prerelease && 'beta' || 'latest' }} 48 | -------------------------------------------------------------------------------- /.github/workflows/storybook.yml: -------------------------------------------------------------------------------- 1 | name: Build & Deploy Storybook 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Set up Node.js 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: '18.x' 21 | 22 | - name: Enable corepack 23 | run: corepack enable 24 | 25 | - name: Set Yarn version 26 | run: yarn set version stable 27 | 28 | - name: Yarn Install 29 | run: yarn install 30 | 31 | - name: Deploy Storybook 32 | uses: chromaui/action@v1 33 | with: 34 | projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} 35 | token: ${{ secrets.GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 100, 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-vite'; 2 | import { mergeConfig } from 'vite'; 3 | import tsConfigPaths from 'vite-tsconfig-paths'; 4 | 5 | const config: StorybookConfig = { 6 | stories: ['../stories/**/*.stories.@(ts|tsx)', '../stories/**/*.mdx'], 7 | addons: ['@storybook/addon-links', '@storybook/addon-essentials'], 8 | framework: { 9 | name: '@storybook/react-vite', 10 | options: {}, 11 | }, 12 | docs: { 13 | autodocs: true, 14 | }, 15 | typescript: { 16 | reactDocgen: 'react-docgen-typescript', 17 | }, 18 | viteFinal(config) { 19 | return mergeConfig(config, { 20 | plugins: [tsConfigPaths()], 21 | }); 22 | }, 23 | }; 24 | export default config; 25 | -------------------------------------------------------------------------------- /.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/manager-api'; 2 | import nhsTheme from './theme'; 3 | import { startCase, upperFirst } from "lodash"; 4 | 5 | const sentenceCase = string => { 6 | if (typeof string !== 'string') return '' 7 | return upperFirst(startCase(string).toLowerCase()) 8 | } 9 | 10 | addons.setConfig({ 11 | sidebar: { 12 | renderLabel: ({ name, type }) => 13 | sentenceCase(name), 14 | }, 15 | theme: nhsTheme 16 | }); 17 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import './storybook.scss'; 2 | import { Preview } from '@storybook/react'; 3 | 4 | const preview: Preview = { 5 | parameters: { 6 | actions: { argTypesRegex: '^on[A-Z].*' }, 7 | options: { 8 | storySort: { 9 | order: [ 10 | 'Welcome', 11 | 'Migration Guides', 12 | 'Form Elements', 13 | 'Content Presentation', 14 | 'Navigation', 15 | 'Layout', 16 | 'Patterns', 17 | ], 18 | }, 19 | }, 20 | }, 21 | }; 22 | export default preview; 23 | -------------------------------------------------------------------------------- /.storybook/storybook.scss: -------------------------------------------------------------------------------- 1 | // Allow current nhsuk styles to override legacy 2 | @import '../node_modules/nhsuk-frontend/packages/nhsuk.scss'; 3 | 4 | .tag-wrapper { 5 | display: flex; 6 | flex-direction: column; 7 | 8 | & > .nhsuk-tag { 9 | margin-bottom: 10px; 10 | 11 | &:last-of-type { 12 | margin-bottom: 0; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.storybook/theme.ts: -------------------------------------------------------------------------------- 1 | import { create } from '@storybook/theming/create'; 2 | const version = require('../package.json').version; 3 | 4 | export default create({ 5 | base: 'light', 6 | 7 | colorPrimary: '#005eb8', 8 | colorSecondary: '#768692', 9 | 10 | // UI 11 | appBg: 'white', 12 | appContentBg: 'white', 13 | appBorderColor: 'grey', 14 | appBorderRadius: 4, 15 | 16 | // Typography 17 | fontCode: 'monospace', 18 | 19 | // Text colors 20 | textColor: '#212b32', 21 | textInverseColor: 'white', 22 | 23 | // Toolbar default and active colors 24 | barTextColor: 'rgba(255,255,255,0.8)', 25 | barSelectedColor: 'rgba(255,255,255,1)', 26 | barBg: '#005eb8', 27 | 28 | // Form colors 29 | inputBg: 'white', 30 | inputBorder: '#425563', 31 | inputTextColor: '#212b32', 32 | inputBorderRadius: 4, 33 | 34 | brandTitle: `NHS.UK React Components (v${version})`, 35 | brandUrl: 'https://github.com/NHSDigital/nhsuk-react-components', 36 | }); 37 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/dist/index.js", 15 | "outFiles": [ 16 | "${workspaceFolder}/**/*.js" 17 | ] 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "eslint.format.enable": true, 4 | "editor.formatOnSave": true, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll": "explicit", 7 | "source.fixAll.eslint": "explicit" 8 | }, 9 | "eslint.validate": ["javascript", "typescript"], 10 | "eslint.codeAction.showDocumentation": { 11 | "enable": true 12 | }, 13 | "eslint.alwaysShowStatus": true, 14 | "eslint.workingDirectories": ["src"], 15 | "typescript.tsdk": "node_modules/typescript/lib" 16 | } 17 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | npmRegistryServer: https://registry.yarnpkg.com 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 NHS England 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bundle-base.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "target": "es6", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "baseUrl": "./", 8 | "types": ["jest", "node"], 9 | "sourceMap": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "allowSyntheticDefaultImports": true, 12 | "esModuleInterop": true, 13 | "skipLibCheck": true, 14 | "strict": true, 15 | "strictNullChecks": true, 16 | "resolveJsonModule": true, 17 | "allowJs": true, 18 | "outDir": "./dist", 19 | "paths": { 20 | "@components/*": ["src/components/*"], 21 | "@content-presentation/*": ["src/components/content-presentation/*"], 22 | "@form-elements/*": ["src/components/form-elements/*"], 23 | "@navigation/*": ["src/components/navigation/*"], 24 | "@typography/*": ["src/components/typography/*"], 25 | "@util/*": ["src/util/*"], 26 | "@patterns/*": ["src/patterns/*"], 27 | "@resources/*": ["src/resources/*"] 28 | } 29 | }, 30 | "include": ["src"], 31 | "exclude": ["node_modules", "**/__tests__", "src/setupTests.ts"] 32 | } 33 | -------------------------------------------------------------------------------- /docs/upgrade-to-3.0.md: -------------------------------------------------------------------------------- 1 | # Upgrading to 3.0 2 | 3 | > v3.0 is an upcoming release, this page is a work in progress. 4 | 5 | There are some breaking changes you'll need to be aware of when upgrading to v3. These are mostly related to us upgrading our dependency on [nhsuk-frontend to v5](https://github.com/nhsuk/nhsuk-frontend/blob/main/CHANGELOG.md#500---16-march-2021) which also includes some breaking changes. 6 | 7 | ## Review Date component is now a pattern 8 | 9 | The `ReviewDate` component has been removed from nhsuk-frontend in version 5.0.0. This component is now a `pattern` in the nhsuk-frontend library. 10 | 11 | The only change is that the Default import has a new path. 12 | 13 | Instead of importing the component from `nhsuk-react-components/lib/components/review-date`, you will now import it from `nhsuk-react-components/lib/patterns/review-date`. 14 | 15 | There are no functional changes to the component, and it works exactly as it did before. 16 | 17 | ```tsx 18 | // Old Import 19 | import { ReviewDate } from "nhsuk-react-components"; 20 | import ReviewDate from "nhsuk-react-components/lib/components/review-date"; 21 | 22 | // New Import 23 | import { ReviewDate } from "nhsuk-react-components"; 24 | import ReviewDate from "nhsuk-react-components/lib/patterns/review-date"; 25 | ``` 26 | 27 | ## NHS Logo PNG Fallback Removed 28 | 29 | The .png fallback for the NHS Logo in the header has been removed. This was to support older versions of Internet Explorer, and is no longer required. 30 | 31 | ## The "Three Columns" option for the Footer component has been removed 32 | 33 | This has been removed due to causing accessibility issues in Safari (see the [upstream issue](https://github.com/nhsuk/nhsuk-frontend/issues/575)). 34 | 35 | ## The `long` variant of the Transactional Service Name component has been removed 36 | 37 | In NHS.UK Frontend v5 and above, the header text now defaults to wrapping underneath the logo without the need for a modifier. It is safe to remove the `long` prop from the `` component. 38 | 39 | ## The `prefixText` prop has been added to the `DoDontList.Item` component 40 | 41 | You can now add prefixed text to each `DoDontList.Item` component by supplying the `prefixText` prop. Items with a `type` of `dont` will automatically have a 'do not' prefix text added in the next major release to align with the NHS.UK frontend library v5. -------------------------------------------------------------------------------- /docs/upgrade-to-5.0.md: -------------------------------------------------------------------------------- 1 | # Upgrading to 5.0 2 | 3 | There are some breaking changes you'll need to be aware of when upgrading to v5. These are mostly related to us upgrading our dependency on [nhsuk-frontend to v9](https://github.com/nhsuk/nhsuk-frontend/blob/main/CHANGELOG.md#901---9-october-2024) which also includes breaking changes. 4 | 5 | ## Breaking changes 6 | 7 | ### Breadcrumbs 8 | 9 | The `Breadcrumbs` component no longer contains its own `` container component. 10 | 11 | Instead, `Breadcrumbs` should be moved inside the existing `` or `
` for your overall page, but before the `
` tag. 12 | 13 | This means that instead of this: 14 | 15 | ``` 16 | 17 | Home 18 | 19 | 20 |
21 | ... 22 |
23 |
24 | ``` 25 | 26 | You should have this: 27 | 28 | ``` 29 | 30 | 31 | Home 32 | 33 |
34 | ... 35 |
36 |
37 | ``` 38 | 39 | ### Back link 40 | 41 | Although no changes were needed in this library, nhsuk-frontend library has a breaking change for consumers of the `BackLink` component. See the [Changelog](https://github.com/nhsuk/nhsuk-frontend/blob/main/CHANGELOG.md#updated-back-link-and-breadcrumbs-pr-1002) 42 | 43 | ## New Features 44 | 45 | ### Warning Button 46 | 47 | A new Warning Button variant has been added to the `Button` component. To use this, set the `warning` prop on `Button`, e.g. 48 | 49 | ``` 50 | 51 | ``` 52 | 53 | ## Fixes 54 | 55 | - Add aria-hidden to responsive table cells that show on small screens, to avoid screenreaders calling out the labels/column headings twice. 56 | - Ensure that headers are aligned to expected standards (if there are fewer than 4 links on the header, the content is left-aligned). 57 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest'); 2 | const { compilerOptions } = require('./tsconfig.json'); 3 | 4 | const jestConfig = { 5 | testEnvironment: 'jsdom', 6 | rootDir: './', 7 | setupFilesAfterEnv: ['/src/setupTests.ts'], 8 | collectCoverageFrom: ['/src/**/*.{ts,tsx}'], 9 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 10 | prefix: '', 11 | }), 12 | transform: { 13 | '^.+\\.(t|j)sx?$': [ 14 | 'ts-jest', 15 | { 16 | babelConfig: { 17 | plugins: ['@babel/plugin-transform-modules-commonjs'], 18 | }, 19 | }, 20 | ], 21 | }, 22 | transformIgnorePatterns: ['node_modules/(?!nhsuk-frontend/packages)'], 23 | }; 24 | 25 | module.exports = jestConfig; 26 | -------------------------------------------------------------------------------- /src/__mocks__/styleMock.ts: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as index from '../index'; 2 | 3 | describe('Index', () => { 4 | it('contains all expected elements', () => { 5 | const sortedIndex = Object.keys(index).sort((a, b) => a.localeCompare(b)); 6 | 7 | expect(sortedIndex).toEqual([ 8 | 'ActionLink', 9 | 'ArrowLeftIcon', 10 | 'ArrowRightCircleIcon', 11 | 'ArrowRightIcon', 12 | 'BackLink', 13 | 'BodyText', 14 | 'Breadcrumb', 15 | 'Button', 16 | 'ButtonLink', 17 | 'Card', 18 | 'CharacterCount', 19 | 'CharacterCountType', 20 | 'Checkboxes', 21 | 'ChevronLeftIcon', 22 | 'ChevronRightIcon', 23 | 'Clearfix', 24 | 'CloseIcon', 25 | 'Col', 26 | 'Container', 27 | 'ContentsList', 28 | 'CrossIcon', 29 | 'DateInput', 30 | 'DefaultButton', 31 | 'Details', 32 | 'DoAndDontList', 33 | 'EmdashIcon', 34 | 'ErrorMessage', 35 | 'ErrorSummary', 36 | 'Fieldset', 37 | 'Footer', 38 | 'Form', 39 | 'FormGroup', 40 | 'Header', 41 | 'Hero', 42 | 'HintText', 43 | 'Images', 44 | 'InsetText', 45 | 'Label', 46 | 'LedeText', 47 | 'MinusIcon', 48 | 'NavAZ', 49 | 'Pagination', 50 | 'Panel', 51 | 'PlusIcon', 52 | 'Radios', 53 | 'ReadingWidth', 54 | 'ReviewDate', 55 | 'Row', 56 | 'SearchIcon', 57 | 'Select', 58 | 'SkipLink', 59 | 'SmallEmdashIcon', 60 | 'SummaryList', 61 | 'Table', 62 | 'Tabs', 63 | 'Tag', 64 | 'Textarea', 65 | 'TextInput', 66 | 'TickIcon', 67 | 'useFormContext', 68 | 'WarningCallout', 69 | ]); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/components/content-presentation/details/Details.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, HTMLProps } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface DetailsProps extends HTMLProps { 5 | expander?: boolean; 6 | } 7 | 8 | interface Details extends FC { 9 | Summary: FC>; 10 | Text: FC>; 11 | ExpanderGroup: FC>; 12 | } 13 | 14 | // TODO: Check if standard NHS.UK polyfill "details.polyfill.js" is required 15 | const Details: Details = ({ className, expander, ...rest }) => ( 16 |
20 | ); 21 | 22 | const DetailsSummary: FC> = ({ className, children, ...rest }) => ( 23 | 24 | {children} 25 | 26 | ); 27 | 28 | const DetailsText: FC> = ({ className, ...rest }) => ( 29 |
30 | ); 31 | 32 | const ExpanderGroup: FC> = ({ className, ...rest }) => ( 33 |
34 | ); 35 | 36 | Details.Summary = DetailsSummary; 37 | Details.Text = DetailsText; 38 | Details.ExpanderGroup = ExpanderGroup; 39 | 40 | export default Details; 41 | -------------------------------------------------------------------------------- /src/components/content-presentation/details/__tests__/Details.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import Details from '../'; 4 | 5 | describe('Details', () => { 6 | it('matches snapshot', () => { 7 | const { container } = render(
); 8 | 9 | expect(container).toMatchSnapshot('StandardDetails'); 10 | }); 11 | 12 | it('matches snapshot - with expander present', () => { 13 | const { container } = render(
); 14 | 15 | expect(container).toMatchSnapshot('ExpanderDetails'); 16 | }); 17 | 18 | it('adds expander classes', () => { 19 | const { container } = render(
); 20 | 21 | expect(container.querySelector('.nhsuk-expander')).toBeTruthy(); 22 | }); 23 | 24 | describe('Details.Summary', () => { 25 | it('matches snapshot', () => { 26 | const { container } = render(Content); 27 | 28 | expect(container).toMatchSnapshot('Details.Summary'); 29 | }); 30 | 31 | it('renders children', () => { 32 | const { container } = render(Content); 33 | const summaryText = container.querySelector('span.nhsuk-details__summary-text'); 34 | 35 | expect(summaryText?.textContent).toBe('Content'); 36 | }); 37 | }); 38 | 39 | describe('Details.Text', () => { 40 | it('matches snapshot', () => { 41 | const { container } = render(Text); 42 | 43 | expect(container).toMatchSnapshot('Details.Text'); 44 | }); 45 | }); 46 | 47 | describe('Details.ExpanderGroup', () => { 48 | it('matches snapshot', () => { 49 | const { container } = render(); 50 | 51 | expect(container).toMatchSnapshot('Details.ExpanderGroup'); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/components/content-presentation/details/__tests__/__snapshots__/Details.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Details Details.ExpanderGroup matches snapshot: Details.ExpanderGroup 1`] = ` 4 |
5 |
8 |
9 | `; 10 | 11 | exports[`Details Details.Summary matches snapshot: Details.Summary 1`] = ` 12 |
13 | 16 | 19 | Content 20 | 21 | 22 |
23 | `; 24 | 25 | exports[`Details Details.Text matches snapshot: Details.Text 1`] = ` 26 |
27 |
30 | Text 31 |
32 |
33 | `; 34 | 35 | exports[`Details matches snapshot - with expander present: ExpanderDetails 1`] = ` 36 |
37 |
40 |
41 | `; 42 | 43 | exports[`Details matches snapshot: StandardDetails 1`] = ` 44 |
45 |
48 |
49 | `; 50 | -------------------------------------------------------------------------------- /src/components/content-presentation/details/index.ts: -------------------------------------------------------------------------------- 1 | import Details from './Details'; 2 | 3 | export default Details; 4 | -------------------------------------------------------------------------------- /src/components/content-presentation/do-and-dont-list/__tests__/__snapshots__/DoAndDontList.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`DoAndDontList DoDontList.Item matches snapshot: DoDontList.Item 1`] = ` 4 |
5 |
  • 6 | 22 | Text 23 |
  • 24 |
    25 | `; 26 | 27 | exports[`DoAndDontList list type "do" matches snapshot: DoDontList-Do 1`] = ` 28 |
    29 |
    32 |

    35 | Do 36 |

    37 |
      40 |
    41 |
    42 | `; 43 | 44 | exports[`DoAndDontList list type "dont" matches snapshot: DoDontList-Dont 1`] = ` 45 |
    46 |
    49 |

    52 | Don't 53 |

    54 |
      57 |
    58 |
    59 | `; 60 | -------------------------------------------------------------------------------- /src/components/content-presentation/do-and-dont-list/index.ts: -------------------------------------------------------------------------------- 1 | import DoAndDontList from './DoAndDontList'; 2 | 3 | export default DoAndDontList; 4 | -------------------------------------------------------------------------------- /src/components/content-presentation/hero/__tests__/__snapshots__/Hero.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Hero Hero.Heading matches snapshot: Hero.Heading 1`] = ` 4 |
    5 |

    8 | Text 9 |

    10 |
    11 | `; 12 | 13 | exports[`Hero Hero.Text matches snapshot: Hero.Text 1`] = ` 14 |
    15 |

    18 | Text 19 |

    20 |
    21 | `; 22 | 23 | exports[`Hero matches snapshot: Hero 1`] = ` 24 |
    25 |
    28 |
    29 | `; 30 | -------------------------------------------------------------------------------- /src/components/content-presentation/hero/index.ts: -------------------------------------------------------------------------------- 1 | import Hero from './Hero'; 2 | 3 | export default Hero; 4 | -------------------------------------------------------------------------------- /src/components/content-presentation/icons/BaseIcon.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, HTMLProps } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | export interface BaseIconSVGProps extends HTMLProps { 5 | iconType?: string; 6 | crossOrigin?: '' | 'anonymous' | 'use-credentials'; 7 | } 8 | 9 | export const BaseIconSVG: FC = ({ 10 | className, 11 | children, 12 | height = 34, 13 | width = 34, 14 | iconType, 15 | ...rest 16 | }) => ( 17 | 28 | ); 29 | -------------------------------------------------------------------------------- /src/components/content-presentation/icons/__tests__/Icons.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import * as Icons from '../'; 4 | 5 | describe('Icons', () => { 6 | it('all icons match snapshots', () => { 7 | Object.entries(Icons).forEach((icon) => { 8 | const [name, Component] = icon; 9 | const Icon = Component as React.FC>; 10 | const { container } = render(); 11 | 12 | expect(container).toMatchSnapshot(name); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/components/content-presentation/icons/index.ts: -------------------------------------------------------------------------------- 1 | export { ArrowLeft } from './individual/ArrowLeft'; 2 | export { ArrowRight } from './individual/ArrowRight'; 3 | export { ArrowRightCircle } from './individual/ArrowRightCircle'; 4 | export { ChevronDown } from './individual/ChevronDown'; 5 | export { ChevronLeft } from './individual/ChevronLeft'; 6 | export { ChevronRight } from './individual/ChevronRight'; 7 | export { ChevronRightCircle } from './individual/ChevronRightCircle'; 8 | export { Close } from './individual/Close'; 9 | export { Cross } from './individual/Cross'; 10 | export { SmallEmdash } from './individual/SmallEmdash'; 11 | export { Emdash } from './individual/Emdash'; 12 | export { Minus } from './individual/Minus'; 13 | export { Plus } from './individual/Plus'; 14 | export { Search } from './individual/Search'; 15 | export { Tick } from './individual/Tick'; 16 | -------------------------------------------------------------------------------- /src/components/content-presentation/icons/individual/ArrowLeft.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { BaseIconSVGProps, BaseIconSVG } from '../BaseIcon'; 3 | 4 | export const ArrowLeft: FC = (props) => ( 5 | 6 | 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /src/components/content-presentation/icons/individual/ArrowRight.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { BaseIconSVGProps, BaseIconSVG } from '../BaseIcon'; 3 | 4 | export const ArrowRight: FC = (props) => ( 5 | 6 | 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /src/components/content-presentation/icons/individual/ArrowRightCircle.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { BaseIconSVGProps, BaseIconSVG } from '../BaseIcon'; 3 | 4 | export const ArrowRightCircle: FC = (props) => ( 5 | 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /src/components/content-presentation/icons/individual/ChevronDown.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { BaseIconSVGProps, BaseIconSVG } from '../BaseIcon'; 3 | 4 | export const ChevronDown: FC = (props) => ( 5 | 6 | 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /src/components/content-presentation/icons/individual/ChevronLeft.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { BaseIconSVGProps, BaseIconSVG } from '../BaseIcon'; 3 | 4 | export const ChevronLeft: FC = (props) => ( 5 | 6 | 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /src/components/content-presentation/icons/individual/ChevronRight.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { BaseIconSVGProps, BaseIconSVG } from '../BaseIcon'; 3 | 4 | export const ChevronRight: FC = (props) => ( 5 | 6 | 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /src/components/content-presentation/icons/individual/ChevronRightCircle.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | export const ChevronRightCircle: FC = () => ( 4 | 25 | ); 26 | -------------------------------------------------------------------------------- /src/components/content-presentation/icons/individual/Close.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { BaseIconSVGProps, BaseIconSVG } from '../BaseIcon'; 3 | 4 | export const Close: FC = (props) => ( 5 | 6 | 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /src/components/content-presentation/icons/individual/Cross.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { BaseIconSVGProps, BaseIconSVG } from '../BaseIcon'; 3 | 4 | export const Cross: FC = (props) => ( 5 | 6 | 10 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /src/components/content-presentation/icons/individual/Emdash.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { BaseIconSVGProps, BaseIconSVG } from '../BaseIcon'; 3 | 4 | export const Emdash: FC = ({ height = 1, width = 19, ...rest }) => ( 5 | 6 | 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /src/components/content-presentation/icons/individual/Minus.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { BaseIconSVGProps, BaseIconSVG } from '../BaseIcon'; 3 | 4 | export const Minus: FC = (props) => ( 5 | 6 | 7 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /src/components/content-presentation/icons/individual/Plus.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { BaseIconSVGProps, BaseIconSVG } from '../BaseIcon'; 3 | 4 | export const Plus: FC = (props) => ( 5 | 6 | 7 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /src/components/content-presentation/icons/individual/Search.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { BaseIconSVGProps, BaseIconSVG } from '../BaseIcon'; 3 | 4 | export const Search: FC = (props) => ( 5 | 6 | 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /src/components/content-presentation/icons/individual/SmallEmdash.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { BaseIconSVGProps, BaseIconSVG } from '../BaseIcon'; 3 | 4 | export const SmallEmdash: FC = ({ height = 1, width = 16, ...rest }) => ( 5 | 6 | 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /src/components/content-presentation/icons/individual/Tick.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { BaseIconSVGProps, BaseIconSVG } from '../BaseIcon'; 3 | 4 | export const Tick: FC = (props) => ( 5 | 6 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /src/components/content-presentation/images/Images.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, HTMLProps } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface ImageProps extends HTMLProps { 5 | // Overriding the default crossOrigin the default is crossOrigin: string | undefined 6 | // which causes a typescript "incompatible types" error. 7 | crossOrigin?: 'anonymous' | 'use-credentials' | undefined; 8 | caption?: string; 9 | } 10 | 11 | const Images: FC = ({ className, caption, ...rest }) => ( 12 |
    13 | {/* eslint-disable-next-line jsx-a11y/alt-text */} 14 | 15 | {caption ?
    {caption}
    : null} 16 |
    17 | ); 18 | 19 | export default Images; 20 | -------------------------------------------------------------------------------- /src/components/content-presentation/images/__tests__/Images.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import Images from '../'; 4 | 5 | describe('Images', () => { 6 | it('matches snapshot', () => { 7 | const { container } = render(); 8 | 9 | expect(container.querySelector('.nhsuk-image')).toBeTruthy(); 10 | expect(container).toMatchSnapshot('Images'); 11 | }); 12 | 13 | it('renders caption', () => { 14 | const { container } = render(); 15 | 16 | expect(container.querySelector('figcaption')).toBeTruthy(); 17 | expect(container.querySelector('figcaption.nhsuk-image__caption')).toBeTruthy(); 18 | expect(container.querySelector('figcaption')?.textContent).toBe('Caption'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/components/content-presentation/images/__tests__/__snapshots__/Images.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Images matches snapshot: Images 1`] = ` 4 |
    5 |
    8 | 11 |
    12 |
    13 | `; 14 | -------------------------------------------------------------------------------- /src/components/content-presentation/images/index.ts: -------------------------------------------------------------------------------- 1 | import Images from './Images'; 2 | 3 | export default Images; 4 | -------------------------------------------------------------------------------- /src/components/content-presentation/inset-text/InsetText.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, HTMLProps } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface InsetTextProps extends HTMLProps { 5 | visuallyHiddenText?: string | false; 6 | } 7 | 8 | const InsetText: FC = ({ 9 | className, 10 | children, 11 | visuallyHiddenText = 'Information: ', 12 | ...rest 13 | }) => ( 14 |
    15 | {visuallyHiddenText ? ( 16 | {visuallyHiddenText} 17 | ) : null} 18 | {children} 19 |
    20 | ); 21 | 22 | export default InsetText; 23 | -------------------------------------------------------------------------------- /src/components/content-presentation/inset-text/__tests__/InsetText.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import InsetText from '../'; 4 | 5 | describe('InsetText', () => { 6 | it('matches snapshot', () => { 7 | const { container } = render(); 8 | 9 | expect(container).toMatchSnapshot('InsetText'); 10 | }); 11 | 12 | it('has default visually hidden text', () => { 13 | const { container } = render(); 14 | 15 | expect(container.querySelector('.nhsuk-u-visually-hidden')?.textContent).toBe('Information: '); 16 | }); 17 | 18 | it('has disabled visually hidden text', () => { 19 | const { container } = render(); 20 | 21 | expect(container.querySelector('.nhsuk-u-visually-hidden')).toBeFalsy(); 22 | }); 23 | 24 | it('has custom visually hidden text', () => { 25 | const { container } = render(); 26 | 27 | expect(container.querySelector('.nhsuk-u-visually-hidden')?.textContent).toBe('Custom'); 28 | }); 29 | 30 | it('renders children', () => { 31 | const { container } = render(Child); 32 | 33 | expect(container.textContent).toBe('Information: Child'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/components/content-presentation/inset-text/__tests__/__snapshots__/InsetText.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`InsetText matches snapshot: InsetText 1`] = ` 4 |
    5 |
    8 | 11 | Information: 12 | 13 |
    14 |
    15 | `; 16 | -------------------------------------------------------------------------------- /src/components/content-presentation/inset-text/index.ts: -------------------------------------------------------------------------------- 1 | import InsetText from './InsetText'; 2 | 3 | export default InsetText; 4 | -------------------------------------------------------------------------------- /src/components/content-presentation/summary-list/SummaryList.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, HTMLProps } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | const SummaryListRow: FC> = ({ className, ...rest }) => ( 5 |
    6 | ); 7 | 8 | const SummaryListKey: FC> = ({ className, ...rest }) => ( 9 |
    10 | ); 11 | 12 | const SummaryListValue: FC> = ({ className, ...rest }) => ( 13 |
    14 | ); 15 | 16 | const SummaryListActions: FC> = ({ className, ...rest }) => ( 17 |
    18 | ); 19 | 20 | interface SummaryListProps extends HTMLProps { 21 | noBorder?: boolean; 22 | } 23 | 24 | interface SummaryList extends FC { 25 | Row: FC>; 26 | Key: FC>; 27 | Value: FC>; 28 | Actions: FC>; 29 | } 30 | 31 | const SummaryList: SummaryList = ({ className, noBorder, ...rest }) => ( 32 |
    40 | ); 41 | 42 | SummaryList.Row = SummaryListRow; 43 | SummaryList.Key = SummaryListKey; 44 | SummaryList.Value = SummaryListValue; 45 | SummaryList.Actions = SummaryListActions; 46 | 47 | export default SummaryList; 48 | -------------------------------------------------------------------------------- /src/components/content-presentation/summary-list/__tests__/SummaryList.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import SummaryList from '../'; 4 | 5 | describe('SummaryList', () => { 6 | it('matches snapshot', () => { 7 | const { container } = render(); 8 | 9 | expect(container).toMatchSnapshot('SummaryList'); 10 | }); 11 | 12 | it('adds css classes when noBorder prop supplied', () => { 13 | const { container } = render(); 14 | 15 | expect(container.querySelector('.nhsuk-summary-list--no-border')).toBeTruthy(); 16 | }); 17 | 18 | describe('SummaryList.Row', () => { 19 | it('matches snapshot', () => { 20 | const { container } = render(Row); 21 | 22 | expect(container.textContent).toBe('Row'); 23 | expect(container).toMatchSnapshot(); 24 | }); 25 | }); 26 | 27 | describe('SummaryList.Key', () => { 28 | it('matches snapshot', () => { 29 | const { container } = render(Key); 30 | 31 | expect(container.textContent).toBe('Key'); 32 | expect(container).toMatchSnapshot(); 33 | }); 34 | }); 35 | 36 | describe('SummaryList.Value', () => { 37 | it('matches snapshot', () => { 38 | const { container } = render(Value); 39 | 40 | expect(container.textContent).toBe('Value'); 41 | expect(container).toMatchSnapshot(); 42 | }); 43 | }); 44 | 45 | describe('SummaryList.Actions', () => { 46 | it('matches snapshot', () => { 47 | const { container } = render(Actions); 48 | 49 | expect(container.textContent).toBe('Actions'); 50 | expect(container).toMatchSnapshot(); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/components/content-presentation/summary-list/__tests__/__snapshots__/SummaryList.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SummaryList SummaryList.Actions matches snapshot 1`] = ` 4 |
    5 |
    8 | Actions 9 |
    10 |
    11 | `; 12 | 13 | exports[`SummaryList SummaryList.Key matches snapshot 1`] = ` 14 |
    15 |
    18 | Key 19 |
    20 |
    21 | `; 22 | 23 | exports[`SummaryList SummaryList.Row matches snapshot 1`] = ` 24 |
    25 |
    28 | Row 29 |
    30 |
    31 | `; 32 | 33 | exports[`SummaryList SummaryList.Value matches snapshot 1`] = ` 34 |
    35 |
    38 | Value 39 |
    40 |
    41 | `; 42 | 43 | exports[`SummaryList matches snapshot: SummaryList 1`] = ` 44 |
    45 |
    48 |
    49 | `; 50 | -------------------------------------------------------------------------------- /src/components/content-presentation/summary-list/index.ts: -------------------------------------------------------------------------------- 1 | import SummaryList from './SummaryList'; 2 | 3 | export default SummaryList; 4 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/Table.tsx: -------------------------------------------------------------------------------- 1 | import React, { ComponentProps, FC, HTMLProps, ReactNode, useMemo, useState } from 'react'; 2 | import classNames from 'classnames'; 3 | import TableBody from './components/TableBody'; 4 | import TableCaption from './components/TableCaption'; 5 | import TableCell, { TableCellProps } from './components/TableCell'; 6 | import TableContainer from './components/TableContainer'; 7 | import TableHead from './components/TableHead'; 8 | import TableRow from './components/TableRow'; 9 | import TablePanel, { TablePanelProps } from './components/TablePanel'; 10 | import TableContext, { ITableContext } from './TableContext'; 11 | 12 | interface TableProps extends HTMLProps { 13 | responsive?: boolean; 14 | caption?: ReactNode; 15 | captionProps?: ComponentProps; 16 | } 17 | 18 | interface Table extends FC { 19 | Body: FC>; 20 | Cell: FC; 21 | Container: FC>; 22 | Head: FC>; 23 | Panel: FC; 24 | Row: FC>; 25 | } 26 | 27 | const Table = ({ 28 | caption, 29 | captionProps, 30 | children, 31 | className, 32 | responsive = false, 33 | ...rest 34 | }: TableProps) => { 35 | const [headings, setHeadings] = useState([]); 36 | 37 | const contextValue: ITableContext = useMemo(() => { 38 | return { 39 | isResponsive: Boolean(responsive), 40 | headings, 41 | setHeadings, 42 | }; 43 | }, [responsive, headings, setHeadings]); 44 | 45 | return ( 46 | 47 | 55 | {caption && {caption}} 56 | {children} 57 |
    58 |
    59 | ); 60 | }; 61 | 62 | Table.Body = TableBody; 63 | Table.Cell = TableCell; 64 | Table.Container = TableContainer; 65 | Table.Head = TableHead; 66 | Table.Panel = TablePanel; 67 | Table.Row = TableRow; 68 | 69 | export default Table; 70 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/TableContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export interface ITableContext { 4 | isResponsive: boolean; 5 | headings: string[]; 6 | setHeadings(headings: string[]): void; 7 | } 8 | 9 | const TableContext = createContext({ 10 | /* eslint-disable @typescript-eslint/no-empty-function */ 11 | isResponsive: false, 12 | headings: [], 13 | setHeadings: () => {}, 14 | }); 15 | 16 | export default TableContext; 17 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/TableHelpers.ts: -------------------------------------------------------------------------------- 1 | import { Children, isValidElement, ReactElement, ReactNode } from 'react'; 2 | import TableCell from './components/TableCell'; 3 | 4 | export const isTableCell = (child: ReactNode): child is ReactElement => { 5 | return isValidElement(child) && child.type === TableCell; 6 | }; 7 | 8 | export const getHeadingsFromChildren = (children: ReactNode): string[] => { 9 | const headings: string[] = []; 10 | Children.map(children, (child) => { 11 | if (isTableCell(child)) { 12 | headings.push(child.props.children.toString()); 13 | } 14 | }); 15 | return headings; 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/TableSectionContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export enum TableSection { 4 | NONE, 5 | HEAD, 6 | BODY, 7 | } 8 | 9 | const TableSectionContext = createContext(TableSection.NONE); 10 | 11 | export default TableSectionContext; 12 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/components/TableBody.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, { FC, HTMLProps } from 'react'; 3 | import TableSectionContext, { TableSection } from '../TableSectionContext'; 4 | 5 | const TableBody: FC> = ({ className, children, ...rest }) => ( 6 | 7 | 8 | {children} 9 | 10 | 11 | ); 12 | TableBody.displayName = 'Table.Body'; 13 | 14 | export default TableBody; 15 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/components/TableCaption.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, HTMLProps } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | const TableCaption: FC> = ({ className, ...rest }) => ( 5 | 6 | ); 7 | TableCaption.displayName = 'Table.Caption'; 8 | 9 | export default TableCaption; 10 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/components/TableCell.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, { FC, HTMLProps, useContext } from 'react'; 3 | import useDevWarning from '@util/hooks/UseDevWarning'; 4 | import TableSectionContext, { TableSection } from '../TableSectionContext'; 5 | 6 | const CellOutsideOfSectionWarning = 7 | 'Table.Cell used outside of a Table.Head or Table.Body component. Unable to determine section type from context.'; 8 | 9 | export interface TableCellProps extends HTMLProps { 10 | _responsive?: boolean; 11 | _responsiveHeading?: string; 12 | isNumeric?: boolean; 13 | } 14 | 15 | const TableCell: FC = ({ 16 | className, 17 | _responsive = false, 18 | _responsiveHeading = '', 19 | isNumeric, 20 | children, 21 | ...rest 22 | }) => { 23 | const section = useContext(TableSectionContext); 24 | useDevWarning(CellOutsideOfSectionWarning, () => section === TableSection.NONE); 25 | 26 | const cellClass = section === TableSection.HEAD ? 'nhsuk-table__header' : 'nhsuk-table__cell'; 27 | const classes = classNames(cellClass, { [`${cellClass}--numeric`]: isNumeric }, className); 28 | 29 | return ( 30 | <> 31 | {section === TableSection.HEAD ? ( 32 | 33 | {children} 34 | 35 | ) : ( 36 | 37 | {_responsive && ( 38 | 39 | {_responsiveHeading} 40 | 41 | )} 42 | {children} 43 | 44 | )} 45 | 46 | ); 47 | }; 48 | 49 | TableCell.displayName = 'Table.Cell'; 50 | 51 | export default TableCell; 52 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/components/TableContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, HTMLProps } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | const TableContainer: FC> = ({ className, ...rest }) => ( 5 |
    6 | ); 7 | TableContainer.displayName = 'Table.Container'; 8 | 9 | export default TableContainer; 10 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/components/TableHead.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, HTMLProps } from 'react'; 2 | import classNames from 'classnames'; 3 | import TableSectionContext, { TableSection } from '../TableSectionContext'; 4 | 5 | const TableHead: FC> = ({ className, children, ...rest }) => ( 6 | 7 | 8 | {children} 9 | 10 | 11 | ); 12 | 13 | TableHead.displayName = 'Table.Head'; 14 | 15 | export default TableHead; 16 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/components/TablePanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, ComponentProps, HTMLProps } from 'react'; 2 | import classNames from 'classnames'; 3 | import HeadingLevel from '@components/utils/HeadingLevel'; 4 | 5 | export interface TablePanelProps extends HTMLProps { 6 | heading?: string; 7 | headingProps?: ComponentProps; 8 | } 9 | 10 | const TablePanel: FC = ({ 11 | className, 12 | heading, 13 | headingProps, 14 | children, 15 | ...rest 16 | }) => ( 17 |
    18 | {heading && ( 19 | 24 | {heading} 25 | 26 | )} 27 | {children} 28 |
    29 | ); 30 | 31 | export default TablePanel; 32 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/components/TableRow.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import classNames from 'classnames'; 3 | import React, { Children, cloneElement, FC, HTMLProps, useContext, useEffect } from 'react'; 4 | import TableContext from '../TableContext'; 5 | import { getHeadingsFromChildren, isTableCell } from '../TableHelpers'; 6 | import TableSectionContext, { TableSection } from '../TableSectionContext'; 7 | 8 | const TableRow: FC> = ({ className, children, ...rest }) => { 9 | const section = useContext(TableSectionContext); 10 | const { isResponsive, headings, setHeadings } = useContext(TableContext); 11 | 12 | useEffect(() => { 13 | if (isResponsive && section === TableSection.HEAD) { 14 | setHeadings(getHeadingsFromChildren(children)); 15 | } 16 | }, [isResponsive, section, children]); 17 | 18 | if (isResponsive && section === TableSection.BODY) { 19 | const tableCells = Children.map(children, (child, index) => { 20 | if (isTableCell(child)) { 21 | return cloneElement(child, { 22 | _responsive: isResponsive, 23 | _responsiveHeading: `${headings[index] || ''} `, 24 | }); 25 | } 26 | return child; 27 | }); 28 | 29 | return ( 30 | 31 | {tableCells} 32 | 33 | ); 34 | } 35 | 36 | return ( 37 | 38 | {children} 39 | 40 | ); 41 | }; 42 | TableRow.displayName = 'Table.Row'; 43 | 44 | export default TableRow; 45 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/components/__tests__/TableBody.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import Table from '../../Table'; 4 | import TableSectionContext, { TableSection } from '../../TableSectionContext'; 5 | import TableBody from '../TableBody'; 6 | 7 | describe('Table.Body', () => { 8 | it('matches snapshot', () => { 9 | const { container } = render( 10 | 11 | 12 |
    , 13 | ); 14 | 15 | expect(container).toMatchSnapshot(); 16 | }); 17 | 18 | it('exposes TableSectionContext', () => { 19 | let tableSection: TableSection = TableSection.NONE; 20 | 21 | const TestComponent = () => { 22 | const tableContext = useContext(TableSectionContext); 23 | 24 | if (tableSection !== tableContext) { 25 | tableSection = tableContext; 26 | } 27 | 28 | return null; 29 | }; 30 | 31 | render( 32 | 33 | 34 | 35 | 36 |
    , 37 | ); 38 | 39 | expect(tableSection).toBe(TableSection.BODY); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/components/__tests__/TableCaption.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import TableCaption from '../TableCaption'; 4 | 5 | describe('TableCaption', () => { 6 | it('matches snapshot', () => { 7 | const { container } = render(); 8 | 9 | expect(container).toMatchSnapshot(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/components/__tests__/TableContainer.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import TableContainer from '../TableContainer'; 4 | 5 | describe('TableContainer', () => { 6 | it('matches snapshot', () => { 7 | const { container } = render(); 8 | 9 | expect(container).toMatchSnapshot(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/components/__tests__/TableHead.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import Table from '../../Table'; 4 | import TableSectionContext, { TableSection } from '../../TableSectionContext'; 5 | import TableHead from '../TableHead'; 6 | 7 | describe('Table.Head', () => { 8 | it('matches snapshot', () => { 9 | const { container } = render(); 10 | 11 | expect(container).toMatchSnapshot(); 12 | }); 13 | 14 | it('exposes TableSectionContext', () => { 15 | let tableSection: TableSection = TableSection.NONE; 16 | 17 | const TestComponent = () => { 18 | const tableContext = useContext(TableSectionContext); 19 | 20 | if (tableSection !== tableContext) { 21 | tableSection = tableContext; 22 | } 23 | 24 | return null; 25 | }; 26 | 27 | render( 28 | 29 | 30 | 31 | 32 |
    , 33 | ); 34 | 35 | expect(tableSection).toBe(TableSection.HEAD); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/components/__tests__/TablePanel.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import TablePanel from '../TablePanel'; 4 | 5 | describe('Table.Panel', () => { 6 | it('matches snapshot', () => { 7 | const { container } = render(); 8 | 9 | expect(container).toMatchSnapshot(); 10 | }); 11 | 12 | it('matches snapshot when rendering a h2 heading', () => { 13 | const { container } = render( 14 | , 15 | ); 16 | 17 | expect(container).toMatchSnapshot(); 18 | }); 19 | 20 | it('adds header when prop added', () => { 21 | const { container } = render( 22 | , 23 | ); 24 | const heading = container.querySelector('h2.nhsuk-table__heading-tab'); 25 | 26 | expect(heading).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/components/__tests__/__snapshots__/TableBody.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Table.Body matches snapshot 1`] = ` 4 |
    5 | 6 | 9 |
    10 |
    11 | `; 12 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/components/__tests__/__snapshots__/TableCaption.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TableCaption matches snapshot 1`] = ` 4 |
    5 | 8 |
    9 | `; 10 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/components/__tests__/__snapshots__/TableCell.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Table.Cell matches snapshot 1`] = ` 4 |
    5 | 8 |
    9 | `; 10 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/components/__tests__/__snapshots__/TableContainer.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TableContainer matches snapshot 1`] = ` 4 |
    5 |
    8 |
    9 | `; 10 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/components/__tests__/__snapshots__/TableHead.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Table.Head matches snapshot 1`] = ` 4 |
    5 | 8 |
    9 | `; 10 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/components/__tests__/__snapshots__/TablePanel.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Table.Panel matches snapshot 1`] = ` 4 |
    5 |
    8 |
    9 | `; 10 | 11 | exports[`Table.Panel matches snapshot when rendering a h2 heading 1`] = ` 12 |
    13 |
    16 |

    19 | TestHeading 20 |

    21 |
    22 |
    23 | `; 24 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/components/__tests__/__snapshots__/TableRow.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Table.Row matches snapshot 1`] = ` 4 |
    5 | 6 | 7 | 10 | 11 |
    12 |
    13 | `; 14 | -------------------------------------------------------------------------------- /src/components/content-presentation/table/index.ts: -------------------------------------------------------------------------------- 1 | import Table from './Table'; 2 | 3 | export default Table; 4 | -------------------------------------------------------------------------------- /src/components/content-presentation/tabs/Tabs.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import classNames from 'classnames'; 3 | import React, { FC, HTMLAttributes, useEffect } from 'react'; 4 | import HeadingLevel, { HeadingLevelType } from '@components/utils/HeadingLevel'; 5 | import TabsJs from '@resources/tabs'; 6 | 7 | type TabsProps = HTMLAttributes; 8 | 9 | type TabTitleProps = { children: React.ReactNode; headingLevel?: HeadingLevelType }; 10 | 11 | type TabListProps = { 12 | children: React.ReactNode; 13 | }; 14 | 15 | type TabListItemProps = { 16 | id: string; 17 | children: React.ReactNode; 18 | }; 19 | 20 | type TabContentsProps = { 21 | id: string; 22 | children: React.ReactNode; 23 | }; 24 | 25 | const TabTitle: FC = ({ children, headingLevel = 'h2' }) => ( 26 | 27 | {children} 28 | 29 | ); 30 | 31 | const TabList: FC = ({ children }) => ( 32 |
      {children}
    33 | ); 34 | 35 | const TabListItem: FC = ({ id, children }) => ( 36 |
  • 37 | 38 | {children} 39 | 40 |
  • 41 | ); 42 | 43 | const TabContents: FC = ({ id, children }) => ( 44 |
    45 | {children} 46 |
    47 | ); 48 | 49 | interface Tabs extends FC { 50 | Title: FC; 51 | List: FC; 52 | ListItem: FC; 53 | Contents: FC; 54 | } 55 | 56 | const Tabs: Tabs = ({ className, children, ...rest }) => { 57 | useEffect(() => { 58 | TabsJs(); 59 | }, []); 60 | 61 | return ( 62 |
    63 | {children} 64 |
    65 | ); 66 | }; 67 | 68 | Tabs.Title = TabTitle; 69 | Tabs.List = TabList; 70 | Tabs.ListItem = TabListItem; 71 | Tabs.Contents = TabContents; 72 | 73 | export default Tabs; 74 | -------------------------------------------------------------------------------- /src/components/content-presentation/tabs/index.ts: -------------------------------------------------------------------------------- 1 | import Tabs from './Tabs'; 2 | 3 | export default Tabs; 4 | -------------------------------------------------------------------------------- /src/components/content-presentation/tag/Tag.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, HTMLProps } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface TagProps extends HTMLProps { 5 | color?: 6 | | 'white' 7 | | 'grey' 8 | | 'green' 9 | | 'aqua-green' 10 | | 'blue' 11 | | 'purple' 12 | | 'pink' 13 | | 'red' 14 | | 'orange' 15 | | 'yellow'; 16 | } 17 | 18 | const Tag: FC = ({ className, color, ...rest }) => ( 19 | 23 | ); 24 | 25 | export default Tag; 26 | -------------------------------------------------------------------------------- /src/components/content-presentation/tag/__tests__/Tag.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { ComponentProps } from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import Tag from '../Tag'; 4 | 5 | describe('Tag', () => { 6 | it('matches snapshot', () => { 7 | const { container } = render(Active); 8 | 9 | expect(container).toMatchSnapshot(); 10 | }); 11 | 12 | it('renders a nhsuk-tag class', () => { 13 | const { container } = render(Active); 14 | 15 | expect(container.querySelector('strong.nhsuk-tag')).toBeTruthy(); 16 | }); 17 | 18 | it.each['color']>([ 19 | 'white', 20 | 'grey', 21 | 'green', 22 | 'aqua-green', 23 | 'blue', 24 | 'purple', 25 | 'pink', 26 | 'red', 27 | 'orange', 28 | 'yellow', 29 | ])('adds colour class %s ', (colour) => { 30 | const { container } = render(); 31 | 32 | expect(container.querySelector(`strong.nhsuk-tag.nhsuk-tag--${colour}`)).toBeTruthy(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/components/content-presentation/tag/__tests__/__snapshots__/Tag.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Tag matches snapshot 1`] = ` 4 |
    5 | 8 | Active 9 | 10 |
    11 | `; 12 | -------------------------------------------------------------------------------- /src/components/content-presentation/tag/index.ts: -------------------------------------------------------------------------------- 1 | import Tag from './Tag'; 2 | 3 | export default Tag; 4 | -------------------------------------------------------------------------------- /src/components/content-presentation/warning-callout/WarningCallout.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, HTMLProps } from 'react'; 2 | import classNames from 'classnames'; 3 | import HeadingLevel, { HeadingLevelType } from '@components/utils/HeadingLevel'; 4 | 5 | interface WarningCalloutLabelProps extends HTMLProps { 6 | headingLevel?: HeadingLevelType; 7 | visuallyHiddenText?: string | false; 8 | } 9 | 10 | const WarningCalloutLabel: FC = ({ 11 | className, 12 | visuallyHiddenText = 'Important: ', 13 | children, 14 | ...rest 15 | }) => ( 16 | 17 | {/* eslint-disable-next-line jsx-a11y/aria-role */} 18 | 19 | {visuallyHiddenText && {visuallyHiddenText}} 20 | {children} 21 | 22 | 23 | ); 24 | 25 | interface IWarningCallout extends FC> { 26 | Label: typeof WarningCalloutLabel; 27 | } 28 | 29 | const WarningCallout: IWarningCallout = ({ className, ...rest }) => ( 30 |
    31 | ); 32 | 33 | WarningCallout.Label = WarningCalloutLabel; 34 | 35 | export default WarningCallout; 36 | -------------------------------------------------------------------------------- /src/components/content-presentation/warning-callout/__tests__/__snapshots__/WarningCallout.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`WarningCallout matches snapshot 1`] = ` 4 |
    5 |
    8 |

    11 | 14 | 17 | Important: 18 | 19 | School, nursery or work 20 | 21 |

    22 |

    23 | Stay away from school, nursery or work until all the spots have crusted over. This is usually 5 days after the spots first appeared. 24 |

    25 |
    26 |
    27 | `; 28 | -------------------------------------------------------------------------------- /src/components/content-presentation/warning-callout/index.ts: -------------------------------------------------------------------------------- 1 | import WarningCallout from './WarningCallout'; 2 | 3 | export default WarningCallout; 4 | -------------------------------------------------------------------------------- /src/components/form-elements/button/index.ts: -------------------------------------------------------------------------------- 1 | import ButtonElement from './Button'; 2 | 3 | export default ButtonElement; 4 | export { Button, ButtonLink } from './Button'; 5 | -------------------------------------------------------------------------------- /src/components/form-elements/character-count/CharacterCount.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { FC, useEffect } from 'react'; 3 | import CharacterCountJs from '@resources/character-count'; 4 | import { HTMLAttributesWithData } from '@util/types/NHSUKTypes'; 5 | 6 | export enum CharacterCountType { 7 | Characters, 8 | Words, 9 | } 10 | 11 | type CharacterCountProps = React.HTMLAttributes & { 12 | children: React.ReactNode; 13 | maxLength: number; 14 | countType: CharacterCountType; 15 | textAreaId: string; 16 | thresholdPercent?: number; 17 | }; 18 | 19 | const CharacterCount: FC = ({ 20 | children, 21 | maxLength, 22 | countType, 23 | textAreaId, 24 | thresholdPercent, 25 | ...rest 26 | }) => { 27 | useEffect(() => { 28 | CharacterCountJs(); 29 | }, []); 30 | 31 | const characterCountProps: HTMLAttributesWithData = 32 | countType === CharacterCountType.Characters 33 | ? { ...rest, ['data-maxlength']: maxLength } 34 | : { ...rest, ['data-maxwords']: maxLength }; 35 | 36 | if (thresholdPercent) { 37 | characterCountProps['data-threshold'] = thresholdPercent; 38 | } 39 | 40 | return ( 41 |
    46 |
    {children}
    47 | 48 |
    49 | You can enter up to {maxLength} characters 50 |
    51 |
    52 | ); 53 | }; 54 | 55 | export default CharacterCount; 56 | -------------------------------------------------------------------------------- /src/components/form-elements/character-count/__tests__/__snapshots__/CharacterCount.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Character Count Matches snapshot 1`] = ` 4 |
    5 |
    10 |
    13 | 19 |
    23 | Do not include personal information like your name, date of birth or NHS number. 24 |
    25 |
    28 | 19 | )} 20 | 21 | ); 22 | 23 | export default Textarea; 24 | -------------------------------------------------------------------------------- /src/components/form-elements/textarea/__tests__/Textarea.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { createRef } from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import Textarea from '../'; 4 | 5 | describe('Textarea', () => { 6 | it('render with a given ref', () => { 7 | const ref = createRef(); 8 | const { container } = render(