diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 00000000..48a4efd1 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,5 @@ +[javascripts] +supports es6-module + +[node] +node 22 diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index c5c41edb..00000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -jest.config.js -.eslintrc.js -rollup.config.mjs -dist \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 97688586..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,41 +0,0 @@ -module.exports = { - parserOptions: { - project: './tsconfig.json', - tsconfigRootDir: __dirname, - }, - env: { - browser: true, - jest: true, - }, - settings: { - 'import/resolver': { - typescript: {}, - }, - react: { - version: 'detect', - }, - }, - extends: [ - 'plugin:react/recommended', - 'plugin:@typescript-eslint/recommended', - 'prettier', - 'plugin:import/errors', - 'plugin:import/warnings', - 'plugin:import/typescript', - 'plugin:jsx-a11y/recommended', - 'plugin:react-hooks/recommended', - ], - rules: { - 'react/prop-types': 0, - 'jsx-a11y/anchor-has-content': 0, - 'jsx-a11y/alt-text': 0, - 'jsx-a11y/heading-has-content': 0, - 'react-hooks/exhaustive-deps': 0, - }, - overrides: [ - { - files: ['*.stories.tsx'], - rules: { '@typescript-eslint/no-unused-vars': 'off' }, - }, - ], -}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aaf9601b..0079701c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,9 +22,9 @@ jobs: - name: Yarn Install run: yarn - name: Lint - run: yarn lint:ci + run: yarn lint - name: Jest Tests - run: yarn test:ci + run: yarn test --coverage - name: Typescript build run: yarn build - name: Storybook build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c3d4732f..da2ef318 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,10 +28,10 @@ jobs: run: yarn - name: Lint - run: yarn lint:ci + run: yarn lint - name: Jest Tests - run: yarn test:ci + run: yarn test --coverage - name: Typescript build run: yarn build diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..3741fba3 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,12 @@ +# Node.js modules +node_modules/ + +# Test coverage +coverage/ + +# Build output +dist/ + +# Files to ignore +.yarnrc.yml +package-lock.json diff --git a/.prettierrc b/.prettierrc index ca8527e0..feb65279 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,8 @@ { + "printWidth": 100, + "quoteProps": "consistent", "semi": true, - "trailingComma": "all", "singleQuote": true, - "printWidth": 100, - "tabWidth": 2 + "tabWidth": 2, + "trailingComma": "all" } diff --git a/.storybook/main.ts b/.storybook/main.ts index f42bd8fe..41dbb818 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -4,21 +4,29 @@ import tsConfigPaths from 'vite-tsconfig-paths'; const config: StorybookConfig = { stories: ['../stories/**/*.stories.@(ts|tsx)', '../stories/**/*.mdx'], - addons: ['@storybook/addon-links', '@storybook/addon-essentials'], + addons: ['@storybook/addon-links', '@storybook/addon-docs'], framework: { name: '@storybook/react-vite', options: {}, }, - docs: { - autodocs: true, - }, + typescript: { reactDocgen: 'react-docgen-typescript', }, + viteFinal(config) { return mergeConfig(config, { plugins: [tsConfigPaths()], + css: { + preprocessorOptions: { + scss: { + quietDeps: true, + loadPaths: ['node_modules'], + }, + }, + }, }); }, }; + export default config; diff --git a/.storybook/manager.ts b/.storybook/manager.ts index 0e2f6b66..2a4e8134 100644 --- a/.storybook/manager.ts +++ b/.storybook/manager.ts @@ -1,15 +1,18 @@ -import { addons } from '@storybook/manager-api'; +import { addons } from 'storybook/manager-api'; import nhsTheme from './theme'; import { startCase, upperFirst } from 'lodash'; -const sentenceCase = (string) => { - if (typeof string !== 'string') return ''; - return upperFirst(startCase(string).toLowerCase()); +const sentenceCase = (name = '') => { + if (!name || typeof name !== 'string') { + return ''; + } + + return upperFirst(startCase(name).toLowerCase()); }; addons.setConfig({ sidebar: { - renderLabel: ({ name, type }) => sentenceCase(name), + renderLabel: ({ name }) => sentenceCase(name), }, theme: nhsTheme, }); diff --git a/.storybook/preview.ts b/.storybook/preview.ts index fce54dde..763bb406 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,5 +1,5 @@ import './storybook.scss'; -import { Preview } from '@storybook/react'; +import { type Preview } from '@storybook/react-vite'; const preview: Preview = { parameters: { @@ -18,5 +18,8 @@ const preview: Preview = { }, }, }, + + tags: ['autodocs'], }; + export default preview; diff --git a/.storybook/storybook.scss b/.storybook/storybook.scss index b168b639..ea4b70a2 100644 --- a/.storybook/storybook.scss +++ b/.storybook/storybook.scss @@ -1,5 +1,4 @@ -// Allow current nhsuk styles to override legacy -@import 'nhsuk-frontend/dist/nhsuk'; +@forward 'nhsuk-frontend/dist/nhsuk'; .tag-wrapper { display: flex; diff --git a/.storybook/theme.ts b/.storybook/theme.ts index 5d3ea651..a7f0b8e9 100644 --- a/.storybook/theme.ts +++ b/.storybook/theme.ts @@ -1,5 +1,5 @@ -import { create } from '@storybook/theming/create'; -const version = require('../package.json').version; +import { create } from 'storybook/theming/create'; +import packageJson from '../package.json' with { type: 'json' }; export default create({ base: 'light', @@ -31,6 +31,6 @@ export default create({ inputTextColor: '#212b32', inputBorderRadius: 4, - brandTitle: `NHS.UK React Components (v${version})`, + brandTitle: `NHS.UK React Components (v${packageJson.version})`, brandUrl: 'https://github.com/NHSDigital/nhsuk-react-components', }); diff --git a/.vscode/settings.json b/.vscode/settings.json index 667ff68e..340494c7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,11 +6,9 @@ "source.fixAll": "explicit", "source.fixAll.eslint": "explicit" }, - "eslint.validate": ["javascript", "typescript"], + "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], "eslint.codeAction.showDocumentation": { "enable": true }, - "eslint.alwaysShowStatus": true, - "eslint.workingDirectories": ["src"], "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/.yarnrc.yml b/.yarnrc.yml index 7af90284..52c22f2d 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1,2 +1,17 @@ nodeLinker: node-modules -npmRegistryServer: https://registry.yarnpkg.com + +npmRegistryServer: "https://registry.yarnpkg.com" + +packageExtensions: + "@storybook/addon-docs@*": + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + + "@storybook/react-vite@*": + peerDependencies: + typescript: "*" + + "storybook@*": + peerDependencies: + "@testing-library/dom": "*" diff --git a/CHANGELOG.md b/CHANGELOG.md index 5141e92b..9c08600f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # NHS.UK React components +## 6.0.0-beta.1 - 8 October 2025 + +This version adds the panel component from NHS.UK frontend v9.3.0 and supports React v19. + +For a full list of changes in this release please refer to the [migration doc](https://github.com/NHSDigital/nhsuk-react-components/blob/main/docs/upgrade-to-6.0.md). + ## 6.0.0-beta.0 - 30 September 2025 This version provides support for nhsuk-frontend version 10. diff --git a/README.md b/README.md index f6708662..4b61a13a 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,13 @@ yarn add nhsuk-react-components ## Usage ```jsx -import React, { PureComponent } from 'react'; +import { PureComponent } from 'react'; // You can import components from the global module import { Button } from 'nhsuk-react-components'; // Or you can import components directly -import Button from 'nhsuk-react-components/lib/components/button'; +import { Button } from 'nhsuk-react-components/dist/esm/components/button'; class GetStartedButton extends PureComponent { render() { @@ -43,6 +43,7 @@ class GetStartedButton extends PureComponent { - [Upgrading to 3.0](/docs/upgrade-to-3.0.md) - [Upgrading to 4.0](/docs/upgrade-to-4.0.md) - [Upgrading to 5.0](/docs/upgrade-to-5.0.md) +- [Upgrading to 6.0](/docs/upgrade-to-6.0.md) ## Maintainers diff --git a/babel.config.cjs b/babel.config.cjs new file mode 100644 index 00000000..114f1daf --- /dev/null +++ b/babel.config.cjs @@ -0,0 +1,69 @@ +const { NODE_ENV } = process.env; + +/** + * Babel config + * + * @type {TransformOptions} + */ +module.exports = { + browserslistEnv: 'javascripts', + presets: [ + [ + '@babel/preset-env', + { + // Apply bug fixes to avoid transforms + bugfixes: true, + + // Apply smaller "loose" transforms for browsers + loose: true, + + // Apply ES module transforms for Jest + // https://jestjs.io/docs/ecmascript-modules + modules: NODE_ENV === 'test' ? 'auto' : false, + }, + ], + [ + '@babel/preset-react', + { + development: NODE_ENV === 'development', + runtime: 'automatic', + useBuiltIns: true, + }, + ], + '@babel/preset-typescript', + ], + env: { + test: { + browserslistEnv: 'node', + plugins: [ + // Override package.json "imports" for Jest to use sources + // otherwise a build step to output `./dist` is necessary + [ + 'module-resolver', + { + alias: { + '#components': './src/components', + '#patterns': './src/patterns', + '#util': './src/util', + 'nhsuk-react-components': './src/index.ts', + }, + }, + ], + // Remove mandatory ES module file extensions for Jest + // https://nodejs.org/api/esm.html#mandatory-file-extensions + [ + 'replace-import-extension', + { + extMapping: { + '.js': '', + }, + }, + ], + ], + }, + }, +}; + +/** + * @import { TransformOptions } from '@babel/core' + */ diff --git a/bundle-base.tsconfig.json b/bundle-base.tsconfig.json deleted file mode 100644 index f2c7791e..00000000 --- a/bundle-base.tsconfig.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "compilerOptions": { - "jsx": "react", - "target": "es6", - "module": "esnext", - "moduleResolution": "node", - "baseUrl": "./", - "types": ["jest", "node"], - "sourceMap": true, - "forceConsistentCasingInFileNames": true, - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "skipLibCheck": true, - "strict": true, - "strictNullChecks": true, - "resolveJsonModule": true, - "allowJs": true, - "outDir": "./dist", - "paths": { - "@components/*": ["src/components/*"], - "@content-presentation/*": ["src/components/content-presentation/*"], - "@form-elements/*": ["src/components/form-elements/*"], - "@navigation/*": ["src/components/navigation/*"], - "@typography/*": ["src/components/typography/*"], - "@util/*": ["src/util/*"], - "@patterns/*": ["src/patterns/*"] - } - }, - "include": ["src"], - "exclude": ["node_modules", "**/__tests__", "src/setupTests.ts"] -} diff --git a/docs/upgrade-to-6.0.md b/docs/upgrade-to-6.0.md index b16402ff..8c09dc05 100644 --- a/docs/upgrade-to-6.0.md +++ b/docs/upgrade-to-6.0.md @@ -6,6 +6,31 @@ There are some breaking changes you'll need to be aware of when upgrading to v6. You must read and apply these updates carefully to make sure your service does not break. +## New features + +### New header component with account section + +The updated [header](https://service-manual.nhs.uk/design-system/components/header) component from NHS.UK frontend v10.x has been added, including support for account information and links. As part of this work we’ve also made some other improvements to the header: + +- show currently active section or page in the navigation +- align navigation items to the left by default +- update navigation label from ’Primary navigation’ to ‘Menu’, and remove superfluous `role` and `id` attributes +- update NHS logo in the header to have higher contrast when focused +- refactor CSS classes and BEM naming, use hidden attributes instead of modifier classes, use generic search element + +### 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. + ## Breaking changes ### Update the JavaScript supported script snippet @@ -210,7 +235,7 @@ To align with NHS.UK frontend, the date input component automatically renders it The custom `autoSelectNext` prop is no longer supported. -### New header component with account section +### Header The updated header component from NHS.UK frontend v10.x has been added. You will need to make the following changes: @@ -464,3 +489,22 @@ To align with NHS.UK frontend, the warning callout `WarningCallout.Label` compon

``` + +## Fixes + +- [#52: Expose header navigation open/close state (with setter)](https://github.com/NHSDigital/nhsuk-react-components/issues/52) +- [#69: Unable to use ref attribute on some components](https://github.com/NHSDigital/nhsuk-react-components/issues/69) +- [#71: Expose FormGroup component to consumers](https://github.com/NHSDigital/nhsuk-react-components/issues/71) +- [#105: getHeadingsFromChildren forces use of string as table cell child](https://github.com/NHSDigital/nhsuk-react-components/issues/105) +- [#166: SkipLink double jumps to first heading then #maincontent if disableDefaultBehaviour is not set](https://github.com/NHSDigital/nhsuk-react-components/issues/166) +- [#174: Responsive tables and validation errors](https://github.com/NHSDigital/nhsuk-react-components/issues/174) +- [#214: Hints and errors are not semantically associated with fieldsets](https://github.com/NHSDigital/nhsuk-react-components/issues/214) +- [#215: Suggestion: remove all 'boolean' examples from storybook](https://github.com/NHSDigital/nhsuk-react-components/issues/215) +- [#243: Use correct NHS.UK frontend JavaScript when rendered client-side](https://github.com/NHSDigital/nhsuk-react-components/issues/243) +- [#244: Breaking change: remove default legend and label sizes or else change to l](https://github.com/NHSDigital/nhsuk-react-components/issues/244) +- [#245: Fieldset incorrectly gets set in error when a child input is in error](https://github.com/NHSDigital/nhsuk-react-components/issues/245) +- [#247: Date component uses label rather than fieldset with legend](https://github.com/NHSDigital/nhsuk-react-components/issues/247) +- [#256: SkipLink does not work if intended target header is rerendered](https://github.com/NHSDigital/nhsuk-react-components/issues/256) +- [#259: Remove pattern="[0-9]\*" from date inputs](https://github.com/NHSDigital/nhsuk-react-components/issues/259) +- [#260: Allow custom component for button links](https://github.com/NHSDigital/nhsuk-react-components/issues/260) +- [#265: Header logo is not labeled correctly when organisation info is provided](https://github.com/NHSDigital/nhsuk-react-components/issues/265) diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..2d8cf8cf --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,103 @@ +import { join } from 'node:path'; +import configPrettier from 'eslint-config-prettier/flat'; +import pluginReact from 'eslint-plugin-react'; +import pluginReactHooks from 'eslint-plugin-react-hooks'; +import eslint from '@eslint/js'; +import pluginJsxA11y from 'eslint-plugin-jsx-a11y'; +import { includeIgnoreFile } from '@eslint/compat'; +import { defineConfig, globalIgnores } from 'eslint/config'; +import globals from 'globals'; +import pluginImport from 'eslint-plugin-import'; +import pluginTypeScript from 'typescript-eslint'; + +const rootPath = import.meta.dirname; +const gitignorePath = join(rootPath, '.gitignore'); + +export default defineConfig([ + { + files: ['**/*.{js,mjs,ts,tsx}'], + extends: [ + configPrettier, + eslint.configs.recommended, + pluginTypeScript.configs.recommended, + pluginImport.flatConfigs.recommended, + pluginImport.flatConfigs.typescript, + ], + languageOptions: { + parser: pluginTypeScript.parser, + parserOptions: { + ecmaVersion: 'latest', + projectService: true, + tsconfigRootDir: rootPath, + }, + }, + rules: { + // Turn off rules that are handled by TypeScript + // https://typescript-eslint.io/troubleshooting/typed-linting/performance/#eslint-plugin-import + 'import/default': 'off', + 'import/named': 'off', + 'import/namespace': 'off', + 'import/no-cycle': 'off', + 'import/no-deprecated': 'off', + 'import/no-named-as-default': 'off', + 'import/no-named-as-default-member': 'off', + 'import/no-unresolved': 'off', + 'import/no-unused-modules': 'off', + }, + settings: { + 'import/resolver': { + node: true, + typescript: true, + }, + }, + }, + { + files: ['**/*.{ts,tsx}'], + extends: [ + pluginJsxA11y.flatConfigs.recommended, + pluginReact.configs.flat.recommended, + pluginReact.configs.flat['jsx-runtime'], + 'react-hooks/recommended-latest', + ], + languageOptions: { + globals: globals.browser, + parserOptions: { + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + plugins: { + 'react-hooks': pluginReactHooks, + }, + settings: { + react: { + version: 'detect', + }, + }, + }, + { + files: ['**/*.{cjs,js,mjs}'], + languageOptions: { + globals: globals.node, + }, + }, + { + files: ['**/*.cjs'], + rules: { + '@typescript-eslint/no-require-imports': 'off', + '@typescript-eslint/no-var-requires': 'off', + }, + }, + { + files: ['**/*.test.{ts,tsx}'], + languageOptions: { + globals: globals.jest, + }, + }, + { + files: ['**/*.stories.tsx'], + rules: { '@typescript-eslint/no-unused-vars': 'off' }, + }, + globalIgnores(['**/coverage/', '**/dist/']), + includeIgnoreFile(gitignorePath, 'Imported .gitignore patterns'), +]); diff --git a/jest.config.js b/jest.config.js index 4c0cc8c5..975ac97a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,24 +1,18 @@ -const { pathsToModuleNameMapper } = require('ts-jest'); -const { compilerOptions } = require('./tsconfig.json'); - -const jestConfig = { - testEnvironment: 'jsdom', - rootDir: './', +/** + * Jest config + * + * @type {Config} + */ +export default { + collectCoverageFrom: ['/src/**/*.{js,mjs,ts,tsx}'], + extensionsToTreatAsEsm: ['.jsx', '.ts', '.tsx'], setupFilesAfterEnv: ['/src/setupTests.ts'], - collectCoverageFrom: ['/src/**/*.{ts,tsx}'], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { - prefix: '', - }), + testEnvironment: 'jsdom', transform: { - '^.+\\.(t|j)sx?$': [ - 'ts-jest', - { - babelConfig: { - plugins: ['@babel/plugin-transform-modules-commonjs'], - }, - }, - ], + '^.+\\.(js|mjs|ts|tsx)$': ['babel-jest', { rootMode: 'upward' }], }, }; -module.exports = jestConfig; +/** + * @import { Config } from 'jest' + */ diff --git a/package.json b/package.json index b5330e40..d0cc1bd2 100644 --- a/package.json +++ b/package.json @@ -1,96 +1,132 @@ { "name": "nhsuk-react-components", - "version": "6.0.0-beta.0", + "version": "6.0.0-beta.1", + "license": "MIT", "author": { "name": "NHS England" }, + "sideEffects": false, + "type": "module", + "imports": { + "#components/*": { + "import": "./dist/esm/components/*", + "require": "./dist/cjs/components/*" + }, + "#components": { + "import": "./dist/esm/components/index.js", + "require": "./dist/cjs/components/index.cjs" + }, + "#patterns/*": { + "import": "./dist/esm/patterns/*", + "require": "./dist/cjs/patterns/*" + }, + "#patterns": { + "import": "./dist/esm/patterns/index.js", + "require": "./dist/cjs/patterns/index.cjs" + }, + "#util/*": { + "import": "./dist/esm/util/*", + "require": "./dist/cjs/util/*" + } + }, + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.cjs" + } + }, + "./*": "./*", + "./package.json": "./package.json" + }, + "main": "dist/cjs/index.cjs", + "module": "dist/esm/index.js", + "types": "dist/esm/index.d.ts", "files": [ - "dist", - "lib" + "src", + "dist" ], - "main": "dist/cjs/index.js", - "module": "dist/esm", - "types": "dist/index.d.ts", "scripts": { - "cleanup": "rm -rf dist/ > /dev/null && rm -rf lib/ > /dev/null", - "storybook": "storybook dev -p 6006", + "cleanup": "rm -rf dist/ > /dev/null", "build": "yarn cleanup && rollup -c", + "storybook": "NODE_ENV=development storybook dev -p 6006", + "build-storybook": "storybook build", "test": "jest", "test:watch": "jest --watch", - "test:ci": "jest --coverage", - "lint": "eslint 'src/**/*.{js,ts,tsx}' 'stories/**/*.{js,ts,tsx}'", - "lint:fix": "eslint 'src/**/*.{js,ts,tsx}' 'stories/**/*.{js,ts,tsx}' --fix", - "lint:ci": "eslint 'src/**/*.{js,ts,tsx}' 'stories/**/*.{js,ts,tsx}'", - "build-storybook": "storybook build", - "prepublishOnly": "yarn lint:ci && yarn test:ci && yarn storybook --smoke-test" + "lint": "yarn lint:types && yarn lint:js && yarn lint:prettier", + "lint:fix": "yarn lint:js:fix && yarn lint:prettier:fix", + "lint:prettier": "prettier --check .", + "lint:prettier:fix": "prettier --write .", + "lint:js": "eslint . --max-warnings 0", + "lint:js:fix": "yarn lint:js --fix", + "lint:types": "tsc --build tsconfig.json --pretty", + "prepublishOnly": "yarn lint && yarn test && yarn storybook --smoke-test" }, - "license": "MIT", "devDependencies": { - "@babel/core": "^7.24.1", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-terser": "^0.4.4", - "@rollup/plugin-typescript": "^11.1.6", - "@storybook/addon-actions": "^8.0.5", - "@storybook/addon-essentials": "^8.0.5", - "@storybook/addon-links": "^8.0.5", - "@storybook/blocks": "^8.0.5", - "@storybook/manager-api": "^8.0.5", - "@storybook/preview-api": "^8.0.5", - "@storybook/react": "^8.0.5", - "@storybook/react-vite": "^8.0.5", - "@storybook/theming": "^8.0.5", - "@testing-library/jest-dom": "^6.4.2", - "@testing-library/react": "^14.2.1", - "@types/babel__core": "^7", - "@types/jest": "^29.5.12", + "@babel/core": "^7.28.4", + "@babel/preset-env": "^7.28.3", + "@babel/preset-react": "^7.27.1", + "@babel/preset-typescript": "^7.27.1", + "@eslint/compat": "^1.4.0", + "@eslint/js": "^9.37.0", + "@rollup/plugin-babel": "^6.0.4", + "@rollup/plugin-commonjs": "^28.0.6", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-typescript": "^12.1.4", + "@storybook/addon-docs": "9.1.10", + "@storybook/addon-links": "^9.1.10", + "@storybook/react-vite": "^9.1.10", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@types/eslint": "^9.6.1", + "@types/jest": "^30.0.0", "@types/jest-axe": "^3.5.9", - "@types/node": "^15.0.2", - "@types/react": "^18.2.60", - "@types/react-dom": "^18.2.19", - "@types/rollup-plugin-peer-deps-external": "^2.2.1", - "@typescript-eslint/eslint-plugin": "^7.1.0", - "@typescript-eslint/parser": "^7.1.0", - "babel-jest": "^29.7.0", - "chromatic": "^6.17.3", - "eslint": "^8.57.0", - "eslint-config-airbnb": "^19.0.4", - "eslint-config-prettier": "^9.1.0", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jsx-a11y": "^6.8.0", - "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", - "jest": "^29.7.0", - "jest-axe": "^8.0.0", - "jest-environment-jsdom": "^29.7.0", + "@types/lodash": "^4.17.20", + "@types/node": "^24.6.2", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.1", + "babel-jest": "^30.2.0", + "babel-plugin-module-resolver": "^5.0.2", + "babel-plugin-replace-import-extension": "^1.1.5", + "classnames": "^2.5.1", + "eslint": "^9.37.0", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^6.1.0", + "globals": "^16.4.0", + "jest": "^30.2.0", + "jest-axe": "^10.0.0", + "jest-environment-jsdom": "^30.2.0", + "lodash": "^4.17.21", "nhsuk-frontend": "^10.0.0", "outdent": "^0.8.0", - "prettier": "^3.2.5", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "regenerator-runtime": "^0.13.7", - "rollup": "^4.13.0", - "rollup-plugin-dts": "^6.1.0", - "rollup-plugin-peer-deps-external": "^2.2.4", + "prettier": "^3.6.2", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "rollup": "^4.52.4", "rollup-plugin-preserve-directives": "^0.4.0", - "rollup-plugin-tsconfig-paths": "^1.5.2", - "sass": "^1.53.0", - "storybook": "^8.0.5", - "ts-jest": "^29.1.2", - "typescript": "5.3.3", - "vite": "^4.5.3", - "vite-tsconfig-paths": "^4.3.2" - }, - "dependencies": { - "classnames": "^2.2.6" + "sass-embedded": "^1.93.2", + "storybook": "^9.1.10", + "tslib": "^2.8.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.45.0", + "vite": "^7.1.9", + "vite-tsconfig-paths": "^5.1.4" }, "peerDependencies": { + "classnames": ">=2.5.0", "nhsuk-frontend": ">=10.0.0 <11.0.0", - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "react": ">=18.2.0", + "react-dom": ">=18.2.0", + "tslib": ">=2.8.0" }, - "packageManager": "yarn@4.1.1" + "packageManager": "yarn@4.10.3" } diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 00000000..fadfee86 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,81 @@ +import { join } from 'node:path'; +import { babel } from '@rollup/plugin-babel'; +import commonjs from '@rollup/plugin-commonjs'; +import { nodeResolve } from '@rollup/plugin-node-resolve'; +import typescript from '@rollup/plugin-typescript'; +import preserveDirectives from 'rollup-plugin-preserve-directives'; +import { defineConfig } from 'rollup'; +import packageJson from './package.json' with { type: 'json' }; +import tsBuildConfig from './tsconfig.build.json' with { type: 'json' }; + +const { outDir } = tsBuildConfig.compilerOptions; +const external = Object.keys(packageJson.peerDependencies); + +export default defineConfig( + /** @satisfies {OutputOptions[]} */ ([ + { + entryFileNames: '[name].cjs', + format: 'cjs', + }, + { + entryFileNames: '[name].js', + format: 'esm', + }, + ]).map( + /** + * Rollup options for each module format + * + * @returns {RollupOptions} + */ + (options) => ({ + input: 'src/index.ts', + output: [ + { + ...options, + dir: join(outDir, options.format), + preserveModules: true, + preserveModulesRoot: 'src', + sourcemap: true, + sourcemapExcludeSources: true, + }, + ], + external: ['react/jsx-runtime', ...external], + jsx: /** @type {const} */ ('react-jsx'), + treeshake: false, + plugins: [ + nodeResolve({ + browser: true, + }), + commonjs(), + typescript({ + tsconfig: 'tsconfig.build.json', + compilerOptions: { + emitDeclarationOnly: true, + outDir: join(outDir, options.format), + }, + }), + preserveDirectives(), + babel({ + babelHelpers: 'bundled', + exclude: 'node_modules/**', + }), + ], + + // Handle warnings as errors + onwarn(warning) { + const { code, message } = warning; + + // Skip warnings about "use client" directives + if (code === 'MODULE_LEVEL_DIRECTIVE' && message.includes(`"use client"`)) { + return; + } + + throw new Error(warning.message, { cause: warning }); + }, + }), + ), +); + +/** + * @import { OutputOptions, RollupOptions } from 'rollup' + */ diff --git a/rollup.config.mjs b/rollup.config.mjs deleted file mode 100644 index 7dc3c972..00000000 --- a/rollup.config.mjs +++ /dev/null @@ -1,89 +0,0 @@ -import resolve from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import typescript from '@rollup/plugin-typescript'; -import terser from '@rollup/plugin-terser'; -import external from 'rollup-plugin-peer-deps-external'; -import { dts } from 'rollup-plugin-dts'; -import tsPaths from 'rollup-plugin-tsconfig-paths'; -import preserveDirectives from 'rollup-plugin-preserve-directives'; - -import tsBuildConfig from './bundle-base.tsconfig.json' with { type: 'json' }; -import packageJson from './package.json' with { type: 'json' }; - -// suppresses warnings printed to console as part of bundling components with directives present. -const onWarnSuppression = { - onwarn(warning, warn) { - if (warning.code === 'MODULE_LEVEL_DIRECTIVE' && warning.message.includes(`"use client"`)) { - return; - } - warn(warning); - }, -}; - -const commonPlugins = [external(), tsPaths(), resolve(), commonjs()]; - -export default [ - // cjs export - { - input: 'src/index.ts', - output: [ - { - file: packageJson.main, - format: 'cjs', - sourcemap: true, - }, - ], - plugins: [ - ...commonPlugins, - typescript({ - tsconfig: 'bundle-base.tsconfig.json', - compilerOptions: { - declaration: false, - }, - }), - terser(), - ], - ...onWarnSuppression, - }, - // esm export - { - input: 'src/index.ts', - output: [ - { - dir: packageJson.module, - format: 'esm', - sourcemap: true, - preserveModules: true, - preserveModulesRoot: 'src', - }, - ], - plugins: [ - ...commonPlugins, - typescript({ - tsconfig: 'bundle-base.tsconfig.json', - compilerOptions: { - declaration: true, - declarationDir: 'dist/esm', - emitDeclarationOnly: true, - outDir: 'dist/esm', - }, - }), - preserveDirectives(), - terser({ compress: { directives: false } }), - ], - ...onWarnSuppression, - }, - // type bundling - { - input: 'src/index.ts', - output: [{ file: 'dist/index.d.ts', format: 'esm' }], - external: [], - plugins: [ - dts({ - compilerOptions: { - paths: tsBuildConfig.compilerOptions.paths, - }, - }), - ], - }, -]; diff --git a/src/__mocks__/styleMock.ts b/src/__mocks__/styleMock.ts deleted file mode 100644 index f053ebf7..00000000 --- a/src/__mocks__/styleMock.ts +++ /dev/null @@ -1 +0,0 @@ -module.exports = {}; diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 2f325f87..f2061729 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -1,10 +1,11 @@ -import * as index from '../index'; +import * as index from '..'; describe('Index', () => { it('contains all expected elements', () => { const sortedIndex = Object.keys(index).sort((a, b) => a.localeCompare(b)); expect(sortedIndex).toEqual([ + '__esModule', // Synthetic default export 'ActionLink', 'ArrowLeftIcon', 'ArrowRightCircleIcon', @@ -14,15 +15,20 @@ describe('Index', () => { 'Breadcrumb', 'Button', 'Card', + 'CardContext', + 'cardTypeIsCareCard', 'CharacterCount', 'Checkboxes', + 'CheckboxesContext', 'ChevronRightCircleIcon', + 'childIsOfComponentType', 'Clearfix', 'Col', 'Container', 'ContentsList', 'CrossIcon', 'DateInput', + 'DateInputContext', 'Details', 'DoAndDontList', 'ErrorMessage', @@ -30,10 +36,15 @@ describe('Index', () => { 'Fieldset', 'Footer', 'Form', + 'FormContext', 'FormGroup', + 'FormGroupContext', 'Header', + 'HeaderContext', + 'HeadingLevel', 'Hero', 'HintText', + 'Icon', 'Images', 'InsetText', 'Label', @@ -41,7 +52,9 @@ describe('Index', () => { 'Legend', 'NavAZ', 'Pagination', + 'Panel', 'Radios', + 'RadiosContext', 'ReadingWidth', 'ReviewDate', 'Row', @@ -50,6 +63,9 @@ describe('Index', () => { 'SkipLink', 'SummaryList', 'Table', + 'TableContext', + 'TableSection', + 'TableSectionContext', 'Tabs', 'Tag', 'Textarea', diff --git a/src/components/content-presentation/details/Details.tsx b/src/components/content-presentation/details/Details.tsx index b5dcfc1b..12c169d6 100644 --- a/src/components/content-presentation/details/Details.tsx +++ b/src/components/content-presentation/details/Details.tsx @@ -1,5 +1,5 @@ -import React, { ComponentPropsWithoutRef, FC, forwardRef } from 'react'; import classNames from 'classnames'; +import { forwardRef, type ComponentPropsWithoutRef, type FC } from 'react'; export interface DetailsProps extends ComponentPropsWithoutRef<'details'> { expander?: boolean; @@ -40,7 +40,7 @@ DetailsSummary.displayName = 'Details.Summary'; DetailsText.displayName = 'Details.Text'; ExpanderGroup.displayName = 'Details.ExpanderGroup'; -export default Object.assign(DetailsComponent, { +export const Details = Object.assign(DetailsComponent, { Summary: DetailsSummary, Text: DetailsText, ExpanderGroup, diff --git a/src/components/content-presentation/details/__tests__/Details.test.tsx b/src/components/content-presentation/details/__tests__/Details.test.tsx index b9e237a0..5e558eea 100644 --- a/src/components/content-presentation/details/__tests__/Details.test.tsx +++ b/src/components/content-presentation/details/__tests__/Details.test.tsx @@ -1,7 +1,7 @@ -import React, { createRef } from 'react'; import { render } from '@testing-library/react'; -import { renderClient, renderServer } from '@util/components'; -import Details from '../'; +import { createRef } from 'react'; +import { Details } from '..'; +import { renderClient, renderServer } from '#util/components'; describe('Details', () => { it('matches snapshot', async () => { diff --git a/src/components/content-presentation/details/__tests__/__snapshots__/Details.test.tsx.snap b/src/components/content-presentation/details/__tests__/__snapshots__/Details.test.tsx.snap index 51e90f28..2850e566 100644 --- a/src/components/content-presentation/details/__tests__/__snapshots__/Details.test.tsx.snap +++ b/src/components/content-presentation/details/__tests__/__snapshots__/Details.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`Details Details.ExpanderGroup matches snapshot: Details.ExpanderGroup 1`] = `
diff --git a/src/components/content-presentation/details/index.ts b/src/components/content-presentation/details/index.ts index 72e3cee2..c55898bc 100644 --- a/src/components/content-presentation/details/index.ts +++ b/src/components/content-presentation/details/index.ts @@ -1 +1 @@ -export { default } from './Details'; +export * from './Details.js'; diff --git a/src/components/content-presentation/do-and-dont-list/DoAndDontList.tsx b/src/components/content-presentation/do-and-dont-list/DoAndDontList.tsx index 9573da7f..7c2a12db 100644 --- a/src/components/content-presentation/do-and-dont-list/DoAndDontList.tsx +++ b/src/components/content-presentation/do-and-dont-list/DoAndDontList.tsx @@ -1,14 +1,16 @@ -import React, { - ComponentPropsWithoutRef, - FC, +'use client'; + +import classNames from 'classnames'; +import { createContext, - useContext, - ReactNode, forwardRef, + useContext, + type ComponentPropsWithoutRef, + type FC, + type ReactNode, } from 'react'; -import classNames from 'classnames'; -import { Tick, Cross } from '@components/content-presentation/icons'; -import HeadingLevel, { HeadingLevelProps } from '@components/utils/HeadingLevel'; +import { CrossIcon, TickIcon } from '../icons/index.js'; +import { HeadingLevel, type HeadingLevelProps } from '#components/utils/HeadingLevel.js'; type ListType = 'do' | 'dont'; @@ -53,12 +55,12 @@ const DoAndDontItem: FC = ({ prefixText, listItemType, child
  • {(listItemType || listItem) === 'do' ? ( <> - + {actualPrefix} ) : ( <> - + {actualPrefix} )} @@ -70,6 +72,6 @@ const DoAndDontItem: FC = ({ prefixText, listItemType, child DoAndDontListComponent.displayName = 'DoAndDontList'; DoAndDontItem.displayName = 'DoAndDontList.Item'; -export default Object.assign(DoAndDontListComponent, { +export const DoAndDontList = Object.assign(DoAndDontListComponent, { Item: DoAndDontItem, }); diff --git a/src/components/content-presentation/do-and-dont-list/__tests__/DoAndDontList.test.tsx b/src/components/content-presentation/do-and-dont-list/__tests__/DoAndDontList.test.tsx index 129c2020..f7519b4f 100644 --- a/src/components/content-presentation/do-and-dont-list/__tests__/DoAndDontList.test.tsx +++ b/src/components/content-presentation/do-and-dont-list/__tests__/DoAndDontList.test.tsx @@ -1,7 +1,7 @@ -import React, { createRef } from 'react'; import { render } from '@testing-library/react'; -import DoAndDontList from '../'; -import { renderClient, renderServer } from '@util/components'; +import { createRef } from 'react'; +import { DoAndDontList } from '..'; +import { renderClient, renderServer } from '#util/components'; describe('DoAndDontList', () => { describe('list type "do"', () => { diff --git a/src/components/content-presentation/do-and-dont-list/__tests__/__snapshots__/DoAndDontList.test.tsx.snap b/src/components/content-presentation/do-and-dont-list/__tests__/__snapshots__/DoAndDontList.test.tsx.snap index f45722b7..549880e7 100644 --- a/src/components/content-presentation/do-and-dont-list/__tests__/__snapshots__/DoAndDontList.test.tsx.snap +++ b/src/components/content-presentation/do-and-dont-list/__tests__/__snapshots__/DoAndDontList.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`DoAndDontList DoDontList.Item matches snapshot: DoDontList.Item 1`] = `
    diff --git a/src/components/content-presentation/do-and-dont-list/index.ts b/src/components/content-presentation/do-and-dont-list/index.ts index f6db6b15..bd8c9192 100644 --- a/src/components/content-presentation/do-and-dont-list/index.ts +++ b/src/components/content-presentation/do-and-dont-list/index.ts @@ -1 +1 @@ -export { default } from './DoAndDontList'; +export * from './DoAndDontList.js'; diff --git a/src/components/content-presentation/hero/Hero.tsx b/src/components/content-presentation/hero/Hero.tsx index ccecc19c..1020ac53 100644 --- a/src/components/content-presentation/hero/Hero.tsx +++ b/src/components/content-presentation/hero/Hero.tsx @@ -1,7 +1,7 @@ -import React, { ComponentPropsWithoutRef, FC, forwardRef } from 'react'; import classNames from 'classnames'; -import { Container, Row, Col } from '../../layout'; -import HeadingLevel, { HeadingLevelProps } from '@components/utils/HeadingLevel'; +import { forwardRef, type ComponentPropsWithoutRef, type FC } from 'react'; +import { Col, Container, Row } from '#components/layout/index.js'; +import { HeadingLevel, type HeadingLevelProps } from '#components/utils/HeadingLevel.js'; export interface HeroContentProps extends ComponentPropsWithoutRef<'div'> { hasImage: boolean; @@ -73,7 +73,7 @@ HeroComponent.displayName = 'Hero'; HeroHeading.displayName = 'Hero.Heading'; HeroText.displayName = 'Hero.Text'; -export default Object.assign(HeroComponent, { +export const Hero = Object.assign(HeroComponent, { Heading: HeroHeading, Text: HeroText, }); diff --git a/src/components/content-presentation/hero/__tests__/Hero.test.tsx b/src/components/content-presentation/hero/__tests__/Hero.test.tsx index 6678abd3..39c661c7 100644 --- a/src/components/content-presentation/hero/__tests__/Hero.test.tsx +++ b/src/components/content-presentation/hero/__tests__/Hero.test.tsx @@ -1,6 +1,6 @@ -import React, { createRef } from 'react'; import { render } from '@testing-library/react'; -import Hero from '..'; +import { createRef } from 'react'; +import { Hero } from '..'; describe('Hero', () => { it('matches snapshot', () => { diff --git a/src/components/content-presentation/hero/__tests__/__snapshots__/Hero.test.tsx.snap b/src/components/content-presentation/hero/__tests__/__snapshots__/Hero.test.tsx.snap index e371cb6c..245ec3d2 100644 --- a/src/components/content-presentation/hero/__tests__/__snapshots__/Hero.test.tsx.snap +++ b/src/components/content-presentation/hero/__tests__/__snapshots__/Hero.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`Hero Hero.Heading matches snapshot: Hero.Heading 1`] = `
    diff --git a/src/components/content-presentation/hero/index.ts b/src/components/content-presentation/hero/index.ts index 9c292bc0..60b8a72d 100644 --- a/src/components/content-presentation/hero/index.ts +++ b/src/components/content-presentation/hero/index.ts @@ -1 +1 @@ -export { default } from './Hero'; +export * from './Hero.js'; diff --git a/src/components/content-presentation/icons/BaseIcon.tsx b/src/components/content-presentation/icons/Icon.tsx similarity index 78% rename from src/components/content-presentation/icons/BaseIcon.tsx rename to src/components/content-presentation/icons/Icon.tsx index 04e7c1bb..ccbbb90d 100644 --- a/src/components/content-presentation/icons/BaseIcon.tsx +++ b/src/components/content-presentation/icons/Icon.tsx @@ -1,7 +1,7 @@ -import React, { ComponentPropsWithoutRef, FC } from 'react'; import classNames from 'classnames'; +import { type ComponentPropsWithoutRef, type FC } from 'react'; -export interface BaseIconSVGProps extends ComponentPropsWithoutRef<'svg'> { +export interface IconProps extends ComponentPropsWithoutRef<'svg'> { title?: string; modifier?: | 'arrow-left' @@ -16,10 +16,10 @@ export interface BaseIconSVGProps extends ComponentPropsWithoutRef<'svg'> { /** * @deprecated Use `modifier` instead. */ - iconType?: BaseIconSVGProps['modifier']; + iconType?: IconProps['modifier']; } -export const BaseIconSVG: FC = ({ +export const Icon: FC = ({ className, children, iconType, diff --git a/src/components/content-presentation/icons/__tests__/Icons.test.tsx b/src/components/content-presentation/icons/__tests__/Icons.test.tsx index 9cdff5e4..41ad773e 100644 --- a/src/components/content-presentation/icons/__tests__/Icons.test.tsx +++ b/src/components/content-presentation/icons/__tests__/Icons.test.tsx @@ -1,10 +1,11 @@ -import React from 'react'; import { render } from '@testing-library/react'; -import * as Icons from '../'; +import * as Icons from '..'; describe('Icons', () => { it('all icons match snapshots', () => { - for (const [name, Icon] of Object.entries(Icons)) { + for (const [name, Icon] of Object.entries(Icons).filter( + ([, Icon]) => Icon instanceof Function, + )) { const { container } = render(); expect(container).toMatchSnapshot(name); } diff --git a/src/components/content-presentation/icons/__tests__/__snapshots__/Icons.test.tsx.snap b/src/components/content-presentation/icons/__tests__/__snapshots__/Icons.test.tsx.snap index f3214c39..49438204 100644 --- a/src/components/content-presentation/icons/__tests__/__snapshots__/Icons.test.tsx.snap +++ b/src/components/content-presentation/icons/__tests__/__snapshots__/Icons.test.tsx.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`Icons all icons match snapshots: ArrowLeft 1`] = ` +exports[`Icons all icons match snapshots: ArrowLeftIcon 1`] = `
    `; -exports[`Icons all icons match snapshots: ArrowRightCircle 1`] = ` +exports[`Icons all icons match snapshots: ArrowRightIcon 1`] = `
    `; -exports[`Icons all icons match snapshots: ChevronRightCircle 1`] = ` +exports[`Icons all icons match snapshots: ChevronRightCircleIcon 1`] = `
    + +
    +`; + +exports[`Icons all icons match snapshots: SearchIcon 1`] = `
    {alt} @@ -14,6 +14,4 @@ const ImagesComponent = forwardRef( ), ); -ImagesComponent.displayName = 'Images'; - -export default ImagesComponent; +Images.displayName = 'Images'; diff --git a/src/components/content-presentation/images/__tests__/Images.test.tsx b/src/components/content-presentation/images/__tests__/Images.test.tsx index 36e8c93e..06137534 100644 --- a/src/components/content-presentation/images/__tests__/Images.test.tsx +++ b/src/components/content-presentation/images/__tests__/Images.test.tsx @@ -1,6 +1,6 @@ -import React, { createRef } from 'react'; import { render } from '@testing-library/react'; -import Images from '../'; +import { createRef } from 'react'; +import { Images } from '..'; describe('Images', () => { it('matches snapshot', () => { diff --git a/src/components/content-presentation/images/__tests__/__snapshots__/Images.test.tsx.snap b/src/components/content-presentation/images/__tests__/__snapshots__/Images.test.tsx.snap index 1a36030e..e4a05a75 100644 --- a/src/components/content-presentation/images/__tests__/__snapshots__/Images.test.tsx.snap +++ b/src/components/content-presentation/images/__tests__/__snapshots__/Images.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`Images matches snapshot: Images 1`] = `
    diff --git a/src/components/content-presentation/images/index.ts b/src/components/content-presentation/images/index.ts index c75230f3..9d399e72 100644 --- a/src/components/content-presentation/images/index.ts +++ b/src/components/content-presentation/images/index.ts @@ -1 +1 @@ -export { default } from './Images'; +export * from './Images.js'; diff --git a/src/components/content-presentation/index.ts b/src/components/content-presentation/index.ts new file mode 100644 index 00000000..55a38c07 --- /dev/null +++ b/src/components/content-presentation/index.ts @@ -0,0 +1,12 @@ +export * from './details/index.js'; +export * from './do-and-dont-list/index.js'; +export * from './hero/index.js'; +export * from './icons/index.js'; +export * from './images/index.js'; +export * from './inset-text/index.js'; +export * from './panel/index.js'; +export * from './summary-list/index.js'; +export * from './table/index.js'; +export * from './tabs/index.js'; +export * from './tag/index.js'; +export * from './warning-callout/index.js'; diff --git a/src/components/content-presentation/inset-text/InsetText.tsx b/src/components/content-presentation/inset-text/InsetText.tsx index cdd0ee5a..40446b65 100644 --- a/src/components/content-presentation/inset-text/InsetText.tsx +++ b/src/components/content-presentation/inset-text/InsetText.tsx @@ -1,9 +1,9 @@ -import React, { ComponentPropsWithoutRef, forwardRef } from 'react'; import classNames from 'classnames'; +import { forwardRef, type ComponentPropsWithoutRef } from 'react'; -type InsetTextProps = ComponentPropsWithoutRef<'div'>; +export type InsetTextProps = ComponentPropsWithoutRef<'div'>; -const InsetTextComponent = forwardRef( +export const InsetText = forwardRef( ({ className, children, ...rest }, forwardedRef) => (
    Information: @@ -12,6 +12,4 @@ const InsetTextComponent = forwardRef( ), ); -InsetTextComponent.displayName = 'InsetText'; - -export default InsetTextComponent; +InsetText.displayName = 'InsetText'; diff --git a/src/components/content-presentation/inset-text/__tests__/InsetText.test.tsx b/src/components/content-presentation/inset-text/__tests__/InsetText.test.tsx index 947a45d4..1e44a5ae 100644 --- a/src/components/content-presentation/inset-text/__tests__/InsetText.test.tsx +++ b/src/components/content-presentation/inset-text/__tests__/InsetText.test.tsx @@ -1,6 +1,6 @@ -import React, { createRef } from 'react'; import { render } from '@testing-library/react'; -import InsetText from '../'; +import { createRef } from 'react'; +import { InsetText } from '..'; describe('InsetText', () => { it('matches snapshot', () => { diff --git a/src/components/content-presentation/inset-text/__tests__/__snapshots__/InsetText.test.tsx.snap b/src/components/content-presentation/inset-text/__tests__/__snapshots__/InsetText.test.tsx.snap index 26e2812e..c8a5225b 100644 --- a/src/components/content-presentation/inset-text/__tests__/__snapshots__/InsetText.test.tsx.snap +++ b/src/components/content-presentation/inset-text/__tests__/__snapshots__/InsetText.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`InsetText matches snapshot: InsetText 1`] = `
    diff --git a/src/components/content-presentation/inset-text/index.ts b/src/components/content-presentation/inset-text/index.ts index 4839c489..1e600b98 100644 --- a/src/components/content-presentation/inset-text/index.ts +++ b/src/components/content-presentation/inset-text/index.ts @@ -1 +1 @@ -export { default } from './InsetText'; +export * from './InsetText.js'; diff --git a/src/components/content-presentation/panel/Panel.tsx b/src/components/content-presentation/panel/Panel.tsx new file mode 100644 index 00000000..ce2d1f35 --- /dev/null +++ b/src/components/content-presentation/panel/Panel.tsx @@ -0,0 +1,36 @@ +import classNames from 'classnames'; +import { Children, forwardRef, type ComponentPropsWithoutRef, type FC } from 'react'; +import { HeadingLevel, type HeadingLevelProps } from '#components/utils/HeadingLevel.js'; +import { childIsOfComponentType } from '#util/types/TypeGuards.js'; + +export type PanelTitleProps = HeadingLevelProps; + +const PanelTitle: FC = ({ children, headingLevel = 'h1', ...rest }) => ( + + {children} + +); + +export type PanelProps = ComponentPropsWithoutRef<'div'>; + +const PanelComponent = forwardRef( + ({ children, className, ...rest }, forwardedRef) => { + const items = Children.toArray(children); + const title = items.find((child) => childIsOfComponentType(child, PanelTitle)); + const bodyItems = items.filter((child) => !childIsOfComponentType(child, PanelTitle)); + + return ( +
    + {title} + {bodyItems ?
    {bodyItems}
    : null} +
    + ); + }, +); + +PanelComponent.displayName = 'Panel'; +PanelComponent.displayName = 'Panel.Title'; + +export const Panel = Object.assign(PanelComponent, { + Title: PanelTitle, +}); diff --git a/src/components/content-presentation/panel/__tests__/Panel.test.tsx b/src/components/content-presentation/panel/__tests__/Panel.test.tsx new file mode 100644 index 00000000..1cfde1a6 --- /dev/null +++ b/src/components/content-presentation/panel/__tests__/Panel.test.tsx @@ -0,0 +1,74 @@ +import { render } from '@testing-library/react'; +import { createRef } from 'react'; +import { Panel, type PanelTitleProps } from '..'; +import { renderClient, renderServer } from '#util/components'; + +describe('Panel', () => { + it('matches snapshot', async () => { + const { container } = await renderClient( + + Booking complete + We have sent you a confirmation email + , + { className: 'nhsuk-panel' }, + ); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot (via server)', async () => { + const { container, element } = await renderServer( + + Booking complete + We have sent you a confirmation email + , + { className: 'nhsuk-panel' }, + ); + + expect(container).toMatchSnapshot('server'); + + await renderClient(element, { + className: 'nhsuk-panel', + hydrate: true, + container, + }); + + expect(container).toMatchSnapshot('client'); + }); + + it('forwards refs', async () => { + const ref = createRef(); + + const { modules } = await renderClient( + + Booking complete + We have sent you a confirmation email + , + { className: 'nhsuk-panel' }, + ); + + const [panelEl] = modules; + + expect(ref.current).toBe(panelEl); + expect(ref.current).toHaveClass('nhsuk-panel'); + }); + + it.each([ + undefined, + { headingLevel: 'h1' }, + { headingLevel: 'h2' }, + { headingLevel: 'h3' }, + { headingLevel: 'h4' }, + ])('renders heading level $headingLevel if specified', (props) => { + const { container } = render( + + Booking complete + We have sent you a confirmation email + , + ); + + const title = container.querySelector('.nhsuk-panel__title'); + + expect(title).toHaveProperty('tagName', props?.headingLevel?.toUpperCase() ?? 'H1'); + }); +}); diff --git a/src/components/content-presentation/panel/__tests__/__snapshots__/Panel.test.tsx.snap b/src/components/content-presentation/panel/__tests__/__snapshots__/Panel.test.tsx.snap new file mode 100644 index 00000000..a2ae20ed --- /dev/null +++ b/src/components/content-presentation/panel/__tests__/__snapshots__/Panel.test.tsx.snap @@ -0,0 +1,58 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Panel matches snapshot (via server): client 1`] = ` +
    +
    +

    + Booking complete +

    +
    + We have sent you a confirmation email +
    +
    +
    +`; + +exports[`Panel matches snapshot (via server): server 1`] = ` +
    +
    +

    + Booking complete +

    +
    + We have sent you a confirmation email +
    +
    +
    +`; + +exports[`Panel matches snapshot 1`] = ` +
    +
    +

    + Booking complete +

    +
    + We have sent you a confirmation email +
    +
    +
    +`; diff --git a/src/components/content-presentation/panel/index.ts b/src/components/content-presentation/panel/index.ts new file mode 100644 index 00000000..81c82d41 --- /dev/null +++ b/src/components/content-presentation/panel/index.ts @@ -0,0 +1 @@ +export * from './Panel.js'; diff --git a/src/components/content-presentation/summary-list/SummaryList.tsx b/src/components/content-presentation/summary-list/SummaryList.tsx index 21bf8da5..97e2e678 100644 --- a/src/components/content-presentation/summary-list/SummaryList.tsx +++ b/src/components/content-presentation/summary-list/SummaryList.tsx @@ -1,5 +1,5 @@ -import React, { ComponentPropsWithoutRef, FC, forwardRef } from 'react'; import classNames from 'classnames'; +import { forwardRef, type ComponentPropsWithoutRef, type FC } from 'react'; const SummaryListRow: FC> = ({ className, ...rest }) => (
    @@ -41,7 +41,7 @@ SummaryListKey.displayName = 'SummaryList.Key'; SummaryListValue.displayName = 'SummaryList.Value'; SummaryListActions.displayName = 'SummaryList.Actions'; -export default Object.assign(SummaryListComponent, { +export const SummaryList = Object.assign(SummaryListComponent, { Row: SummaryListRow, Key: SummaryListKey, Value: SummaryListValue, diff --git a/src/components/content-presentation/summary-list/__tests__/SummaryList.test.tsx b/src/components/content-presentation/summary-list/__tests__/SummaryList.test.tsx index 00568961..a34c39b4 100644 --- a/src/components/content-presentation/summary-list/__tests__/SummaryList.test.tsx +++ b/src/components/content-presentation/summary-list/__tests__/SummaryList.test.tsx @@ -1,6 +1,6 @@ -import React, { createRef } from 'react'; import { render } from '@testing-library/react'; -import SummaryList from '../'; +import { createRef } from 'react'; +import { SummaryList } from '..'; describe('SummaryList', () => { it('matches snapshot', () => { diff --git a/src/components/content-presentation/summary-list/__tests__/__snapshots__/SummaryList.test.tsx.snap b/src/components/content-presentation/summary-list/__tests__/__snapshots__/SummaryList.test.tsx.snap index e8e99c41..5cf97f27 100644 --- a/src/components/content-presentation/summary-list/__tests__/__snapshots__/SummaryList.test.tsx.snap +++ b/src/components/content-presentation/summary-list/__tests__/__snapshots__/SummaryList.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`SummaryList SummaryList.Actions matches snapshot 1`] = `
    diff --git a/src/components/content-presentation/summary-list/index.ts b/src/components/content-presentation/summary-list/index.ts index b5626639..7a6b7689 100644 --- a/src/components/content-presentation/summary-list/index.ts +++ b/src/components/content-presentation/summary-list/index.ts @@ -1 +1 @@ -export { default } from './SummaryList'; +export * from './SummaryList.js'; diff --git a/src/components/content-presentation/table/Table.tsx b/src/components/content-presentation/table/Table.tsx index 6dde1217..07bad0ae 100644 --- a/src/components/content-presentation/table/Table.tsx +++ b/src/components/content-presentation/table/Table.tsx @@ -1,13 +1,24 @@ -import React, { ComponentPropsWithoutRef, ReactNode, forwardRef, useMemo, useState } from 'react'; +'use client'; + import classNames from 'classnames'; -import TableBody from './components/TableBody'; -import TableCaption, { TableCaptionProps } from './components/TableCaption'; -import TableCell from './components/TableCell'; -import TableContainer from './components/TableContainer'; -import TableHead from './components/TableHead'; -import TableRow from './components/TableRow'; -import TablePanel from './components/TablePanel'; -import TableContext, { ITableContext } from './TableContext'; +import { + forwardRef, + useMemo, + useState, + type ComponentPropsWithoutRef, + type ReactNode, +} from 'react'; +import { + TableBody, + TableCaption, + TableCell, + TableContainer, + TableHead, + TablePanel, + TableRow, + type TableCaptionProps, +} from './components/index.js'; +import { TableContext, type ITableContext } from './TableContext.js'; export interface TableProps extends ComponentPropsWithoutRef<'table'> { firstCellIsHeader?: boolean; @@ -36,7 +47,7 @@ const TableComponent = forwardRef((props, forwarde responsive, setHeadings, }; - }, [responsive, headings, setHeadings]); + }, [firstCellIsHeader, headings, responsive, setHeadings]); return ( @@ -59,7 +70,7 @@ const TableComponent = forwardRef((props, forwarde TableComponent.displayName = 'Table'; -export default Object.assign(TableComponent, { +export const Table = Object.assign(TableComponent, { Container: TableContainer, Panel: TablePanel, Head: TableHead, diff --git a/src/components/content-presentation/table/TableContext.ts b/src/components/content-presentation/table/TableContext.ts index 1b2c027c..779d58c0 100644 --- a/src/components/content-presentation/table/TableContext.ts +++ b/src/components/content-presentation/table/TableContext.ts @@ -1,4 +1,6 @@ -import { ReactNode, createContext } from 'react'; +'use client'; + +import { createContext, type ReactNode } from 'react'; export interface ITableContext { firstCellIsHeader: boolean; @@ -7,12 +9,9 @@ export interface ITableContext { setHeadings(headings: ReactNode[]): void; } -const TableContext = createContext({ - /* eslint-disable @typescript-eslint/no-empty-function */ +export const TableContext = createContext({ firstCellIsHeader: false, headings: [], responsive: false, setHeadings: () => {}, }); - -export default TableContext; diff --git a/src/components/content-presentation/table/TableHelpers.ts b/src/components/content-presentation/table/TableHelpers.ts index ba818b77..6ce79057 100644 --- a/src/components/content-presentation/table/TableHelpers.ts +++ b/src/components/content-presentation/table/TableHelpers.ts @@ -1,10 +1,11 @@ -import { Children, isValidElement, ReactElement, ReactNode } from 'react'; -import TableCell, { TableCellProps } from './components/TableCell'; +import { Children, type ReactElement, type ReactNode } from 'react'; +import { TableCell, type TableCellProps } from './components/TableCell.js'; +import { childIsOfComponentType } from '#util/types/TypeGuards.js'; export const isTableCell = ( child: ReactNode, ): child is ReactElement => { - return isValidElement(child) && child.type === TableCell; + return childIsOfComponentType(child, TableCell); }; export const getHeadingsFromChildren = (children?: ReactNode) => { diff --git a/src/components/content-presentation/table/TableSectionContext.ts b/src/components/content-presentation/table/TableSectionContext.ts index b812916d..86b80b5c 100644 --- a/src/components/content-presentation/table/TableSectionContext.ts +++ b/src/components/content-presentation/table/TableSectionContext.ts @@ -1,3 +1,5 @@ +'use client'; + import { createContext } from 'react'; export enum TableSection { @@ -6,6 +8,4 @@ export enum TableSection { BODY, } -const TableSectionContext = createContext(TableSection.NONE); - -export default TableSectionContext; +export const TableSectionContext = createContext(TableSection.NONE); diff --git a/src/components/content-presentation/table/components/TableBody.tsx b/src/components/content-presentation/table/components/TableBody.tsx index 9034b151..606465c9 100644 --- a/src/components/content-presentation/table/components/TableBody.tsx +++ b/src/components/content-presentation/table/components/TableBody.tsx @@ -1,8 +1,10 @@ import classNames from 'classnames'; -import React, { ComponentPropsWithoutRef, FC } from 'react'; -import TableSectionContext, { TableSection } from '../TableSectionContext'; +import { type ComponentPropsWithoutRef, type FC } from 'react'; +import { TableSection, TableSectionContext } from '../TableSectionContext.js'; -const TableBody: FC> = ({ children, className, ...rest }) => ( +export type TableBodyProps = ComponentPropsWithoutRef<'tbody'>; + +export const TableBody: FC = ({ children, className, ...rest }) => ( {children} @@ -11,5 +13,3 @@ const TableBody: FC> = ({ children, className, ); TableBody.displayName = 'Table.Body'; - -export default TableBody; diff --git a/src/components/content-presentation/table/components/TableCaption.tsx b/src/components/content-presentation/table/components/TableCaption.tsx index 6374171f..b5949cd7 100644 --- a/src/components/content-presentation/table/components/TableCaption.tsx +++ b/src/components/content-presentation/table/components/TableCaption.tsx @@ -1,12 +1,12 @@ -import React, { ComponentPropsWithoutRef, FC } from 'react'; import classNames from 'classnames'; -import { NHSUKSize } from '@util/types/NHSUKTypes'; +import { type ComponentPropsWithoutRef, type FC } from 'react'; +import { type NHSUKSize } from '#util/types/NHSUKTypes.js'; export interface TableCaptionProps extends ComponentPropsWithoutRef<'caption'> { size?: NHSUKSize; } -const TableCaption: FC = ({ className, size, ...rest }) => ( +export const TableCaption: FC = ({ className, size, ...rest }) => ( = ({ className, size, ...rest }) => ( ); TableCaption.displayName = 'Table.Caption'; - -export default TableCaption; diff --git a/src/components/content-presentation/table/components/TableCell.tsx b/src/components/content-presentation/table/components/TableCell.tsx index 68398938..07288afc 100644 --- a/src/components/content-presentation/table/components/TableCell.tsx +++ b/src/components/content-presentation/table/components/TableCell.tsx @@ -1,8 +1,10 @@ +'use client'; + import classNames from 'classnames'; -import React, { ComponentPropsWithoutRef, FC, useContext } from 'react'; -import useDevWarning from '@util/hooks/UseDevWarning'; -import TableContext, { ITableContext } from '../TableContext'; -import TableSectionContext, { TableSection } from '../TableSectionContext'; +import { useContext, type ComponentPropsWithoutRef, type FC } from 'react'; +import { TableContext, type ITableContext } from '../TableContext.js'; +import { TableSection, TableSectionContext } from '../TableSectionContext.js'; +import { useDevWarning } from '#util/hooks/index.js'; const CellOutsideOfSectionWarning = 'Table.Cell used outside of a Table.Head or Table.Body component. Unable to determine section type from context.'; @@ -14,7 +16,13 @@ export interface TableCellProps format?: 'numeric'; } -const TableCell: FC = ({ className, format, children, index = -1, ...rest }) => { +export const TableCell: FC = ({ + className, + format, + children, + index = -1, + ...rest +}) => { const { firstCellIsHeader, headings, responsive } = useContext(TableContext); const section = useContext(TableSectionContext); @@ -61,5 +69,3 @@ const TableCell: FC = ({ className, format, children, index = -1 }; TableCell.displayName = 'Table.Cell'; - -export default TableCell; diff --git a/src/components/content-presentation/table/components/TableContainer.tsx b/src/components/content-presentation/table/components/TableContainer.tsx index a538062d..eb201da4 100644 --- a/src/components/content-presentation/table/components/TableContainer.tsx +++ b/src/components/content-presentation/table/components/TableContainer.tsx @@ -1,14 +1,12 @@ -import React, { ComponentPropsWithoutRef, forwardRef } from 'react'; import classNames from 'classnames'; +import { forwardRef, type ComponentPropsWithoutRef } from 'react'; export type TableContainerProps = ComponentPropsWithoutRef<'div'>; -const TableContainer = forwardRef( +export const TableContainer = forwardRef( ({ className, ...rest }, forwardedRef) => (
    ), ); TableContainer.displayName = 'Table.Container'; - -export default TableContainer; diff --git a/src/components/content-presentation/table/components/TableHead.tsx b/src/components/content-presentation/table/components/TableHead.tsx index 40ea8c42..cc5b7d21 100644 --- a/src/components/content-presentation/table/components/TableHead.tsx +++ b/src/components/content-presentation/table/components/TableHead.tsx @@ -1,9 +1,13 @@ -import React, { ComponentPropsWithoutRef, FC, useContext } from 'react'; +'use client'; + import classNames from 'classnames'; -import TableContext, { ITableContext } from '../TableContext'; -import TableSectionContext, { TableSection } from '../TableSectionContext'; +import { useContext, type ComponentPropsWithoutRef, type FC } from 'react'; +import { TableContext, type ITableContext } from '../TableContext.js'; +import { TableSection, TableSectionContext } from '../TableSectionContext.js'; + +export type TableHeadProps = ComponentPropsWithoutRef<'thead'>; -const TableHead: FC> = ({ children, className, ...rest }) => { +export const TableHead: FC = ({ children, className, ...rest }) => { const { responsive } = useContext(TableContext); return ( @@ -20,5 +24,3 @@ const TableHead: FC> = ({ children, className, }; TableHead.displayName = 'Table.Head'; - -export default TableHead; diff --git a/src/components/content-presentation/table/components/TablePanel.tsx b/src/components/content-presentation/table/components/TablePanel.tsx index 9484e4a9..2f9eb3e6 100644 --- a/src/components/content-presentation/table/components/TablePanel.tsx +++ b/src/components/content-presentation/table/components/TablePanel.tsx @@ -1,13 +1,13 @@ -import React, { ComponentPropsWithoutRef, FC } from 'react'; import classNames from 'classnames'; -import HeadingLevel, { HeadingLevelProps } from '@components/utils/HeadingLevel'; +import { type ComponentPropsWithoutRef, type FC } from 'react'; +import { HeadingLevel, type HeadingLevelProps } from '#components/utils/HeadingLevel.js'; export interface TablePanelProps extends ComponentPropsWithoutRef<'div'> { heading?: string; headingProps?: HeadingLevelProps; } -const TablePanel: FC = ({ +export const TablePanel: FC = ({ className, heading, headingProps, @@ -28,5 +28,3 @@ const TablePanel: FC = ({ ); TablePanel.displayName = 'Table.Panel'; - -export default TablePanel; diff --git a/src/components/content-presentation/table/components/TableRow.tsx b/src/components/content-presentation/table/components/TableRow.tsx index 4567fb6d..027b30c4 100644 --- a/src/components/content-presentation/table/components/TableRow.tsx +++ b/src/components/content-presentation/table/components/TableRow.tsx @@ -1,17 +1,19 @@ +'use client'; + import classNames from 'classnames'; -import React, { +import { Children, - ComponentPropsWithoutRef, - FC, cloneElement, useContext, useEffect, + type ComponentPropsWithoutRef, + type FC, } from 'react'; -import TableContext from '../TableContext'; -import { getHeadingsFromChildren, isTableCell } from '../TableHelpers'; -import TableSectionContext, { TableSection } from '../TableSectionContext'; +import { TableContext } from '../TableContext.js'; +import { TableSection, TableSectionContext } from '../TableSectionContext.js'; +import { getHeadingsFromChildren, isTableCell } from '../TableHelpers.js'; -const TableRow: FC> = ({ children, className, ...rest }) => { +export const TableRow: FC> = ({ children, className, ...rest }) => { const section = useContext(TableSectionContext); const { responsive, setHeadings } = useContext(TableContext); @@ -19,7 +21,7 @@ const TableRow: FC> = ({ children, className, ... if (responsive && section === TableSection.HEAD) { setHeadings(getHeadingsFromChildren(children)); } - }, [responsive, section, children]); + }, [children, responsive, section, setHeadings]); const tableCells = Children.map(children, (child, index) => { return section === TableSection.BODY && isTableCell(child) @@ -39,5 +41,3 @@ const TableRow: FC> = ({ children, className, ... }; TableRow.displayName = 'Table.Row'; - -export default TableRow; diff --git a/src/components/content-presentation/table/components/__tests__/Table.test.tsx b/src/components/content-presentation/table/components/__tests__/Table.test.tsx index 68c2bc76..3fe50890 100644 --- a/src/components/content-presentation/table/components/__tests__/Table.test.tsx +++ b/src/components/content-presentation/table/components/__tests__/Table.test.tsx @@ -1,6 +1,6 @@ -import React, { createRef } from 'react'; -import { renderClient, renderServer } from '@util/components'; -import Table from '../..'; +import { createRef } from 'react'; +import { Table } from '../..'; +import { renderClient, renderServer } from '#util/components'; describe('Table', () => { const Example = (props: Parameters[0]) => ( diff --git a/src/components/content-presentation/table/components/__tests__/TableBody.test.tsx b/src/components/content-presentation/table/components/__tests__/TableBody.test.tsx index ef92099b..90fe2042 100644 --- a/src/components/content-presentation/table/components/__tests__/TableBody.test.tsx +++ b/src/components/content-presentation/table/components/__tests__/TableBody.test.tsx @@ -1,8 +1,6 @@ -import React, { useContext } from 'react'; import { render } from '@testing-library/react'; -import Table from '../../Table'; -import TableSectionContext, { TableSection } from '../../TableSectionContext'; -import TableBody from '../TableBody'; +import { TableBody } from '..'; +import { Table, TableSection, TableSectionContext } from '../..'; describe('Table.Body', () => { it('matches snapshot', () => { @@ -16,26 +14,20 @@ describe('Table.Body', () => { }); it('exposes TableSectionContext', () => { - let tableSection: TableSection = TableSection.NONE; - - const TestComponent = () => { - const tableContext = useContext(TableSectionContext); - - if (tableSection !== tableContext) { - tableSection = tableContext; - } - - return null; - }; + const mock = jest.fn(); render( - + + {(section) => { + return mock(section); + }} +
    , ); - expect(tableSection).toBe(TableSection.BODY); + expect(mock).toHaveBeenCalledWith(TableSection.BODY); }); }); diff --git a/src/components/content-presentation/table/components/__tests__/TableCaption.test.tsx b/src/components/content-presentation/table/components/__tests__/TableCaption.test.tsx index 2fc87820..99d64b40 100644 --- a/src/components/content-presentation/table/components/__tests__/TableCaption.test.tsx +++ b/src/components/content-presentation/table/components/__tests__/TableCaption.test.tsx @@ -1,8 +1,7 @@ -import React from 'react'; import { render } from '@testing-library/react'; -import { NHSUKSize } from '@util/types/NHSUKTypes'; -import Table from '../..'; -import TableCaption from '../TableCaption'; +import { TableCaption } from '..'; +import { Table } from '../..'; +import { type NHSUKSize } from '#util/types/NHSUKTypes'; describe('TableCaption', () => { it('matches snapshot', () => { diff --git a/src/components/content-presentation/table/components/__tests__/TableCell.test.tsx b/src/components/content-presentation/table/components/__tests__/TableCell.test.tsx index 7df71ca3..28495043 100644 --- a/src/components/content-presentation/table/components/__tests__/TableCell.test.tsx +++ b/src/components/content-presentation/table/components/__tests__/TableCell.test.tsx @@ -1,6 +1,5 @@ -import React from 'react'; import { render } from '@testing-library/react'; -import Table from '../../Table'; +import { Table } from '../..'; describe('Table.Cell', () => { it('matches snapshot', () => { @@ -30,9 +29,7 @@ describe('Table.Cell', () => { , ); - // eslint-disable-next-line no-console expect(console.warn).toHaveBeenCalledTimes(1); - // eslint-disable-next-line no-console expect(console.warn).toHaveBeenLastCalledWith( 'Table.Cell used outside of a Table.Head or Table.Body component. Unable to determine section type from context.', ); diff --git a/src/components/content-presentation/table/components/__tests__/TableContainer.test.tsx b/src/components/content-presentation/table/components/__tests__/TableContainer.test.tsx index 26536195..88e80167 100644 --- a/src/components/content-presentation/table/components/__tests__/TableContainer.test.tsx +++ b/src/components/content-presentation/table/components/__tests__/TableContainer.test.tsx @@ -1,6 +1,6 @@ -import React, { createRef } from 'react'; import { render } from '@testing-library/react'; -import TableContainer from '../TableContainer'; +import { createRef } from 'react'; +import { TableContainer } from '..'; describe('TableContainer', () => { it('matches snapshot', () => { diff --git a/src/components/content-presentation/table/components/__tests__/TableHead.test.tsx b/src/components/content-presentation/table/components/__tests__/TableHead.test.tsx index 430692e5..5897dfc8 100644 --- a/src/components/content-presentation/table/components/__tests__/TableHead.test.tsx +++ b/src/components/content-presentation/table/components/__tests__/TableHead.test.tsx @@ -1,8 +1,6 @@ -import React, { useContext } from 'react'; import { render } from '@testing-library/react'; -import Table from '../../Table'; -import TableSectionContext, { TableSection } from '../../TableSectionContext'; -import TableHead from '../TableHead'; +import { TableHead } from '..'; +import { Table, TableSection, TableSectionContext } from '../..'; describe('Table.Head', () => { it('matches snapshot', () => { @@ -16,26 +14,20 @@ describe('Table.Head', () => { }); it('exposes TableSectionContext', () => { - let tableSection: TableSection = TableSection.NONE; - - const TestComponent = () => { - const tableContext = useContext(TableSectionContext); - - if (tableSection !== tableContext) { - tableSection = tableContext; - } - - return null; - }; + const mock = jest.fn(); render( - + + {(section) => { + return mock(section); + }} +
    , ); - expect(tableSection).toBe(TableSection.HEAD); + expect(mock).toHaveBeenCalledWith(TableSection.HEAD); }); }); diff --git a/src/components/content-presentation/table/components/__tests__/TablePanel.test.tsx b/src/components/content-presentation/table/components/__tests__/TablePanel.test.tsx index b31c65d8..b8dbd79f 100644 --- a/src/components/content-presentation/table/components/__tests__/TablePanel.test.tsx +++ b/src/components/content-presentation/table/components/__tests__/TablePanel.test.tsx @@ -1,6 +1,5 @@ -import React from 'react'; import { render } from '@testing-library/react'; -import TablePanel from '../TablePanel'; +import { TablePanel } from '..'; describe('Table.Panel', () => { it('matches snapshot', () => { diff --git a/src/components/content-presentation/table/components/__tests__/TableRow.test.tsx b/src/components/content-presentation/table/components/__tests__/TableRow.test.tsx index f40695c9..466501fd 100644 --- a/src/components/content-presentation/table/components/__tests__/TableRow.test.tsx +++ b/src/components/content-presentation/table/components/__tests__/TableRow.test.tsx @@ -1,9 +1,6 @@ import { render } from '@testing-library/react'; -import React from 'react'; -import TableContext, { ITableContext } from '../../TableContext'; -import TableSectionContext, { TableSection } from '../../TableSectionContext'; -import TableCell from '../TableCell'; -import TableRow from '../TableRow'; +import { TableCell, TableRow } from '..'; +import { TableContext, TableSection, TableSectionContext, type ITableContext } from '../..'; const assertCellText = (container: HTMLElement, cellNumber: number, text: string) => { expect(container.querySelector(`[data-test="cell-${cellNumber}"]`)).toHaveTextContent(text); diff --git a/src/components/content-presentation/table/components/__tests__/__snapshots__/Table.test.tsx.snap b/src/components/content-presentation/table/components/__tests__/__snapshots__/Table.test.tsx.snap index bc88b88a..9d3761f4 100644 --- a/src/components/content-presentation/table/components/__tests__/__snapshots__/Table.test.tsx.snap +++ b/src/components/content-presentation/table/components/__tests__/__snapshots__/Table.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`Table matches snapshot (via server): client 1`] = `
    diff --git a/src/components/content-presentation/table/components/__tests__/__snapshots__/TableBody.test.tsx.snap b/src/components/content-presentation/table/components/__tests__/__snapshots__/TableBody.test.tsx.snap index 4ecef708..e624e64f 100644 --- a/src/components/content-presentation/table/components/__tests__/__snapshots__/TableBody.test.tsx.snap +++ b/src/components/content-presentation/table/components/__tests__/__snapshots__/TableBody.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`Table.Body matches snapshot 1`] = `
    diff --git a/src/components/content-presentation/table/components/__tests__/__snapshots__/TableCaption.test.tsx.snap b/src/components/content-presentation/table/components/__tests__/__snapshots__/TableCaption.test.tsx.snap index d175dfb3..08d8ede7 100644 --- a/src/components/content-presentation/table/components/__tests__/__snapshots__/TableCaption.test.tsx.snap +++ b/src/components/content-presentation/table/components/__tests__/__snapshots__/TableCaption.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`TableCaption matches snapshot 1`] = `
    diff --git a/src/components/content-presentation/table/components/__tests__/__snapshots__/TableCell.test.tsx.snap b/src/components/content-presentation/table/components/__tests__/__snapshots__/TableCell.test.tsx.snap index d636c4f2..6282b0c3 100644 --- a/src/components/content-presentation/table/components/__tests__/__snapshots__/TableCell.test.tsx.snap +++ b/src/components/content-presentation/table/components/__tests__/__snapshots__/TableCell.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`Table.Cell matches snapshot 1`] = `
    diff --git a/src/components/content-presentation/table/components/__tests__/__snapshots__/TableContainer.test.tsx.snap b/src/components/content-presentation/table/components/__tests__/__snapshots__/TableContainer.test.tsx.snap index 194e45d3..e2ee6f95 100644 --- a/src/components/content-presentation/table/components/__tests__/__snapshots__/TableContainer.test.tsx.snap +++ b/src/components/content-presentation/table/components/__tests__/__snapshots__/TableContainer.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`TableContainer matches snapshot 1`] = `
    diff --git a/src/components/content-presentation/table/components/__tests__/__snapshots__/TableHead.test.tsx.snap b/src/components/content-presentation/table/components/__tests__/__snapshots__/TableHead.test.tsx.snap index 90e2ca0b..a50cbd3c 100644 --- a/src/components/content-presentation/table/components/__tests__/__snapshots__/TableHead.test.tsx.snap +++ b/src/components/content-presentation/table/components/__tests__/__snapshots__/TableHead.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`Table.Head matches snapshot 1`] = `
    diff --git a/src/components/content-presentation/table/components/__tests__/__snapshots__/TablePanel.test.tsx.snap b/src/components/content-presentation/table/components/__tests__/__snapshots__/TablePanel.test.tsx.snap index 60177755..717a293c 100644 --- a/src/components/content-presentation/table/components/__tests__/__snapshots__/TablePanel.test.tsx.snap +++ b/src/components/content-presentation/table/components/__tests__/__snapshots__/TablePanel.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`Table.Panel matches snapshot 1`] = `
    diff --git a/src/components/content-presentation/table/components/__tests__/__snapshots__/TableRow.test.tsx.snap b/src/components/content-presentation/table/components/__tests__/__snapshots__/TableRow.test.tsx.snap index 7c15e8b4..562a13c8 100644 --- a/src/components/content-presentation/table/components/__tests__/__snapshots__/TableRow.test.tsx.snap +++ b/src/components/content-presentation/table/components/__tests__/__snapshots__/TableRow.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`Table.Row matches snapshot 1`] = `
    diff --git a/src/components/content-presentation/table/components/index.ts b/src/components/content-presentation/table/components/index.ts new file mode 100644 index 00000000..bd87735e --- /dev/null +++ b/src/components/content-presentation/table/components/index.ts @@ -0,0 +1,7 @@ +export * from './TableBody.js'; +export * from './TableCaption.js'; +export * from './TableCell.js'; +export * from './TableContainer.js'; +export * from './TableHead.js'; +export * from './TablePanel.js'; +export * from './TableRow.js'; diff --git a/src/components/content-presentation/table/index.ts b/src/components/content-presentation/table/index.ts index ae769447..f90a825e 100644 --- a/src/components/content-presentation/table/index.ts +++ b/src/components/content-presentation/table/index.ts @@ -1 +1,3 @@ -export { default } from './Table'; +export * from './TableContext.js'; +export * from './TableSectionContext.js'; +export * from './Table.js'; diff --git a/src/components/content-presentation/tabs/Tabs.tsx b/src/components/content-presentation/tabs/Tabs.tsx index b5fcf045..9fbcaf56 100644 --- a/src/components/content-presentation/tabs/Tabs.tsx +++ b/src/components/content-presentation/tabs/Tabs.tsx @@ -1,14 +1,16 @@ +'use client'; + import classNames from 'classnames'; -import React, { - ComponentPropsWithoutRef, - FC, +import { type Tabs as TabsModule } from 'nhsuk-frontend'; +import { createRef, forwardRef, useEffect, useState, + type ComponentPropsWithoutRef, + type FC, } from 'react'; -import HeadingLevel, { HeadingLevelProps } from '@components/utils/HeadingLevel'; -import { type Tabs } from 'nhsuk-frontend'; +import { HeadingLevel, type HeadingLevelProps } from '#components/utils/HeadingLevel.js'; export type TabsProps = ComponentPropsWithoutRef<'div'>; @@ -54,7 +56,7 @@ const TabsComponent = forwardRef((props, forwardedRef const { children, className, ...rest } = props; const [moduleRef] = useState(() => forwardedRef || createRef()); - const [instance, setInstance] = useState(); + const [instance, setInstance] = useState(); useEffect(() => { if (!('current' in moduleRef) || !moduleRef.current || instance) { @@ -86,7 +88,7 @@ TabList.displayName = 'Tabs.List'; TabListItem.displayName = 'Tabs.ListItem'; TabContents.displayName = 'Tabs.Contents'; -export default Object.assign(TabsComponent, { +export const Tabs = Object.assign(TabsComponent, { Title: TabTitle, List: TabList, ListItem: TabListItem, diff --git a/src/components/content-presentation/tabs/__tests__/Tabs.test.tsx b/src/components/content-presentation/tabs/__tests__/Tabs.test.tsx index c9508666..552b6937 100644 --- a/src/components/content-presentation/tabs/__tests__/Tabs.test.tsx +++ b/src/components/content-presentation/tabs/__tests__/Tabs.test.tsx @@ -1,7 +1,7 @@ -import React, { createRef } from 'react'; import { render } from '@testing-library/react'; -import { renderClient, renderServer } from '@util/components'; -import Tabs, { TabTitleProps } from '../Tabs'; +import { createRef } from 'react'; +import { Tabs, type TabTitleProps } from '..'; +import { renderClient, renderServer } from '#util/components'; describe('Tabs', () => { it('matches snapshot', async () => { diff --git a/src/components/content-presentation/tabs/__tests__/__snapshots__/Tabs.test.tsx.snap b/src/components/content-presentation/tabs/__tests__/__snapshots__/Tabs.test.tsx.snap index 6455e409..ab80edc2 100644 --- a/src/components/content-presentation/tabs/__tests__/__snapshots__/Tabs.test.tsx.snap +++ b/src/components/content-presentation/tabs/__tests__/__snapshots__/Tabs.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`Tabs matches snapshot (via server): client 1`] = `
    diff --git a/src/components/content-presentation/tabs/index.ts b/src/components/content-presentation/tabs/index.ts index bc6749b1..f91d05e5 100644 --- a/src/components/content-presentation/tabs/index.ts +++ b/src/components/content-presentation/tabs/index.ts @@ -1 +1 @@ -export { default } from './Tabs'; +export * from './Tabs.js'; diff --git a/src/components/content-presentation/tag/Tag.tsx b/src/components/content-presentation/tag/Tag.tsx index 5bdab605..60d83a6c 100644 --- a/src/components/content-presentation/tag/Tag.tsx +++ b/src/components/content-presentation/tag/Tag.tsx @@ -1,5 +1,5 @@ -import React, { ComponentPropsWithoutRef, FC } from 'react'; import classNames from 'classnames'; +import { type ComponentPropsWithoutRef, type FC } from 'react'; export interface TagProps extends ComponentPropsWithoutRef<'strong'> { modifier?: @@ -20,13 +20,11 @@ export interface TagProps extends ComponentPropsWithoutRef<'strong'> { color?: TagProps['modifier']; } -const TagComponent: FC = ({ className, color, modifier = color, ...rest }) => ( +export const Tag: FC = ({ className, color, modifier = color, ...rest }) => ( ); -TagComponent.displayName = 'Tag'; - -export default TagComponent; +Tag.displayName = 'Tag'; diff --git a/src/components/content-presentation/tag/__tests__/Tag.test.tsx b/src/components/content-presentation/tag/__tests__/Tag.test.tsx index 419e2a96..2cf77408 100644 --- a/src/components/content-presentation/tag/__tests__/Tag.test.tsx +++ b/src/components/content-presentation/tag/__tests__/Tag.test.tsx @@ -1,6 +1,6 @@ -import React, { ComponentPropsWithoutRef } from 'react'; import { render } from '@testing-library/react'; -import Tag from '../Tag'; +import { type ComponentPropsWithoutRef } from 'react'; +import { Tag } from '..'; describe('Tag', () => { it('matches snapshot', () => { diff --git a/src/components/content-presentation/tag/__tests__/__snapshots__/Tag.test.tsx.snap b/src/components/content-presentation/tag/__tests__/__snapshots__/Tag.test.tsx.snap index d0ec444e..7d78fc40 100644 --- a/src/components/content-presentation/tag/__tests__/__snapshots__/Tag.test.tsx.snap +++ b/src/components/content-presentation/tag/__tests__/__snapshots__/Tag.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`Tag matches snapshot 1`] = `
    diff --git a/src/components/content-presentation/tag/index.ts b/src/components/content-presentation/tag/index.ts index 15774bff..17abdf90 100644 --- a/src/components/content-presentation/tag/index.ts +++ b/src/components/content-presentation/tag/index.ts @@ -1 +1 @@ -export { default } from './Tag'; +export * from './Tag.js'; diff --git a/src/components/content-presentation/warning-callout/WarningCallout.tsx b/src/components/content-presentation/warning-callout/WarningCallout.tsx index 47d36608..80b238bf 100644 --- a/src/components/content-presentation/warning-callout/WarningCallout.tsx +++ b/src/components/content-presentation/warning-callout/WarningCallout.tsx @@ -1,6 +1,6 @@ -import React, { ComponentPropsWithoutRef, FC, forwardRef } from 'react'; import classNames from 'classnames'; -import HeadingLevel, { HeadingLevelProps } from '@components/utils/HeadingLevel'; +import { forwardRef, type ComponentPropsWithoutRef, type FC } from 'react'; +import { HeadingLevel, type HeadingLevelProps } from '#components/utils/HeadingLevel.js'; const WarningCalloutHeading: FC = ({ children, className, ...rest }) => ( @@ -21,7 +21,7 @@ const WarningCalloutHeading: FC = ({ children, className, ... ); -type WarningCalloutProps = ComponentPropsWithoutRef<'div'>; +export type WarningCalloutProps = ComponentPropsWithoutRef<'div'>; const WarningCalloutComponent = forwardRef( ({ className, ...rest }, forwardedRef) => ( @@ -32,6 +32,6 @@ const WarningCalloutComponent = forwardRef( WarningCalloutComponent.displayName = 'WarningCallout'; WarningCalloutHeading.displayName = 'WarningCallout.Heading'; -export default Object.assign(WarningCalloutComponent, { +export const WarningCallout = Object.assign(WarningCalloutComponent, { Heading: WarningCalloutHeading, }); diff --git a/src/components/content-presentation/warning-callout/__tests__/WarningCallout.test.tsx b/src/components/content-presentation/warning-callout/__tests__/WarningCallout.test.tsx index 1c2b8109..1edee5f1 100644 --- a/src/components/content-presentation/warning-callout/__tests__/WarningCallout.test.tsx +++ b/src/components/content-presentation/warning-callout/__tests__/WarningCallout.test.tsx @@ -1,6 +1,6 @@ -import React, { createRef } from 'react'; import { render } from '@testing-library/react'; -import WarningCallout from '../WarningCallout'; +import { createRef } from 'react'; +import { WarningCallout } from '..'; describe('WarningCallout', () => { it('matches snapshot', () => { diff --git a/src/components/content-presentation/warning-callout/__tests__/__snapshots__/WarningCallout.test.tsx.snap b/src/components/content-presentation/warning-callout/__tests__/__snapshots__/WarningCallout.test.tsx.snap index d9aad134..768b5efb 100644 --- a/src/components/content-presentation/warning-callout/__tests__/__snapshots__/WarningCallout.test.tsx.snap +++ b/src/components/content-presentation/warning-callout/__tests__/__snapshots__/WarningCallout.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`WarningCallout adds visually hidden text when necessary: WarningCalloutWithTextRole 1`] = `
    diff --git a/src/components/content-presentation/warning-callout/index.ts b/src/components/content-presentation/warning-callout/index.ts index 8886011c..c136d037 100644 --- a/src/components/content-presentation/warning-callout/index.ts +++ b/src/components/content-presentation/warning-callout/index.ts @@ -1 +1 @@ -export { default } from './WarningCallout'; +export * from './WarningCallout.js'; diff --git a/src/components/form-elements/button/Button.tsx b/src/components/form-elements/button/Button.tsx index 6f3ce2a0..56d07bbe 100644 --- a/src/components/form-elements/button/Button.tsx +++ b/src/components/form-elements/button/Button.tsx @@ -1,7 +1,16 @@ -import React, { ForwardedRef, MouseEvent, useEffect, useState, forwardRef, createRef } from 'react'; -import { AsElementLink } from '@util/types/LinkTypes'; -import { type Button } from 'nhsuk-frontend'; +'use client'; + import classNames from 'classnames'; +import { type Button as ButtonModule } from 'nhsuk-frontend'; +import { + createRef, + forwardRef, + useEffect, + useState, + type ForwardedRef, + type MouseEvent, +} from 'react'; +import { type AsElementLink } from '#util/types/LinkTypes.js'; export interface ButtonProps extends AsElementLink { href?: never; @@ -37,7 +46,7 @@ const ButtonComponent = forwardRef((props, forwa } = props; const [moduleRef] = useState(() => forwardedRef || createRef()); - const [instance, setInstance] = useState
    @@ -241,7 +238,6 @@ exports[`DateInput matches snapshot 1`] = ` inputmode="numeric" name="date-input-day" type="text" - value="" />
    @@ -264,7 +260,6 @@ exports[`DateInput matches snapshot 1`] = ` inputmode="numeric" name="date-input-month" type="text" - value="" />
    @@ -287,7 +282,6 @@ exports[`DateInput matches snapshot 1`] = ` inputmode="numeric" name="date-input-year" type="text" - value="" />
    @@ -453,7 +447,6 @@ exports[`DateInput matches snapshot with custom date fields and error message 1` inputmode="numeric" name="date-input-day" type="text" - value="" />
    @@ -565,7 +558,6 @@ exports[`DateInput matches snapshot with error message 1`] = ` inputmode="numeric" name="date-input-day" type="text" - value="" />
    @@ -588,7 +580,6 @@ exports[`DateInput matches snapshot with error message 1`] = ` inputmode="numeric" name="date-input-month" type="text" - value="" />
    @@ -611,7 +602,6 @@ exports[`DateInput matches snapshot with error message 1`] = ` inputmode="numeric" name="date-input-year" type="text" - value="" />
    diff --git a/src/components/form-elements/date-input/components/IndividualDateInputs.tsx b/src/components/form-elements/date-input/components/IndividualDateInputs.tsx index a9be9d74..176436d6 100644 --- a/src/components/form-elements/date-input/components/IndividualDateInputs.tsx +++ b/src/components/form-elements/date-input/components/IndividualDateInputs.tsx @@ -1,8 +1,10 @@ -import React, { ComponentPropsWithoutRef, useContext, ChangeEvent, forwardRef } from 'react'; +'use client'; + import classNames from 'classnames'; -import { FormElementProps } from '@util/types/FormTypes'; -import Label from '../../label/Label'; -import DateInputContext, { IDateInputContext } from '../DateInputContext'; +import { forwardRef, useContext, type ChangeEvent, type ComponentPropsWithoutRef } from 'react'; +import { DateInputContext, type IDateInputContext } from '../DateInputContext.js'; +import { Label } from '#components/form-elements/label/index.js'; +import { type FormElementProps } from '#util/types/FormTypes.js'; export interface IndividualDateInputProps extends ComponentPropsWithoutRef<'input'>, diff --git a/src/components/form-elements/date-input/components/index.ts b/src/components/form-elements/date-input/components/index.ts new file mode 100644 index 00000000..18eb91bb --- /dev/null +++ b/src/components/form-elements/date-input/components/index.ts @@ -0,0 +1 @@ +export * from './IndividualDateInputs.js'; diff --git a/src/components/form-elements/date-input/index.ts b/src/components/form-elements/date-input/index.ts index a8bd7bc4..8d099bb3 100644 --- a/src/components/form-elements/date-input/index.ts +++ b/src/components/form-elements/date-input/index.ts @@ -1 +1,2 @@ -export { default } from './DateInput'; +export * from './DateInputContext.js'; +export * from './DateInput.js'; diff --git a/src/components/form-elements/error-message/ErrorMessage.tsx b/src/components/form-elements/error-message/ErrorMessage.tsx index 8d24b116..1ea1cb20 100644 --- a/src/components/form-elements/error-message/ErrorMessage.tsx +++ b/src/components/form-elements/error-message/ErrorMessage.tsx @@ -1,11 +1,11 @@ -import React, { ComponentPropsWithoutRef, FC } from 'react'; import classNames from 'classnames'; +import { type ComponentPropsWithoutRef, type FC } from 'react'; export interface ErrorMessageProps extends ComponentPropsWithoutRef<'span'> { visuallyHiddenText?: string; } -const ErrorMessageComponent: FC = ({ +export const ErrorMessage: FC = ({ className, visuallyHiddenText = 'Error', children, @@ -28,6 +28,4 @@ const ErrorMessageComponent: FC = ({ ); }; -ErrorMessageComponent.displayName = 'ErrorMessage'; - -export default ErrorMessageComponent; +ErrorMessage.displayName = 'ErrorMessage'; diff --git a/src/components/form-elements/error-message/__tests__/ErrorMessage.test.tsx b/src/components/form-elements/error-message/__tests__/ErrorMessage.test.tsx index e4a33884..c4fa106c 100644 --- a/src/components/form-elements/error-message/__tests__/ErrorMessage.test.tsx +++ b/src/components/form-elements/error-message/__tests__/ErrorMessage.test.tsx @@ -1,6 +1,5 @@ -import React from 'react'; import { render } from '@testing-library/react'; -import ErrorMessage from '../'; +import { ErrorMessage } from '..'; describe('ErrorMessage', () => { it('matches snapshot', () => { diff --git a/src/components/form-elements/error-message/__tests__/__snapshots__/ErrorMessage.test.tsx.snap b/src/components/form-elements/error-message/__tests__/__snapshots__/ErrorMessage.test.tsx.snap index 35874a3f..c6a3f548 100644 --- a/src/components/form-elements/error-message/__tests__/__snapshots__/ErrorMessage.test.tsx.snap +++ b/src/components/form-elements/error-message/__tests__/__snapshots__/ErrorMessage.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`ErrorMessage matches snapshot: ErrorMessage 1`] = `
    diff --git a/src/components/form-elements/error-message/index.ts b/src/components/form-elements/error-message/index.ts index 43c8c9cc..35ed2aa3 100644 --- a/src/components/form-elements/error-message/index.ts +++ b/src/components/form-elements/error-message/index.ts @@ -1 +1 @@ -export { default } from './ErrorMessage'; +export * from './ErrorMessage.js'; diff --git a/src/components/form-elements/error-summary/ErrorSummary.tsx b/src/components/form-elements/error-summary/ErrorSummary.tsx index e4761b5b..0ba53b5d 100644 --- a/src/components/form-elements/error-summary/ErrorSummary.tsx +++ b/src/components/form-elements/error-summary/ErrorSummary.tsx @@ -1,16 +1,18 @@ -import React, { +'use client'; + +import classNames from 'classnames'; +import { type ErrorSummary as ErrorSummaryModule } from 'nhsuk-frontend'; +import { Children, - ComponentPropsWithoutRef, - FC, createRef, forwardRef, - useState, useEffect, + useState, + type ComponentPropsWithoutRef, + type FC, } from 'react'; -import classNames from 'classnames'; -import { AsElementLink } from '@util/types/LinkTypes'; -import { childIsOfComponentType } from '@util/types/TypeGuards'; -import { type ErrorSummary } from 'nhsuk-frontend'; +import { childIsOfComponentType } from '#util/types/TypeGuards.js'; +import { type AsElementLink } from '#util/types/LinkTypes.js'; export type TitleProps = ComponentPropsWithoutRef<'h2'>; @@ -65,7 +67,7 @@ export interface ErrorSummaryProps extends ComponentPropsWithoutRef<'div'> { const ErrorSummaryComponent = forwardRef( ({ children, className, disableAutoFocus, ...rest }, forwardedRef) => { const [moduleRef] = useState(() => forwardedRef || createRef()); - const [instance, setInstance] = useState(); + const [instance, setInstance] = useState(); useEffect(() => { if (!('current' in moduleRef) || !moduleRef.current || instance) { @@ -107,7 +109,7 @@ Title.displayName = 'ErrorSummary.Title'; List.displayName = 'ErrorSummary.List'; ListItem.displayName = 'ErrorSummary.ListItem'; -export default Object.assign(ErrorSummaryComponent, { +export const ErrorSummary = Object.assign(ErrorSummaryComponent, { Title, List, ListItem, diff --git a/src/components/form-elements/error-summary/__tests__/ErrorSummary.test.tsx b/src/components/form-elements/error-summary/__tests__/ErrorSummary.test.tsx index 504774a5..9525c2be 100644 --- a/src/components/form-elements/error-summary/__tests__/ErrorSummary.test.tsx +++ b/src/components/form-elements/error-summary/__tests__/ErrorSummary.test.tsx @@ -1,7 +1,7 @@ -import React, { createRef } from 'react'; import { render } from '@testing-library/react'; -import { renderClient, renderServer } from '@util/components'; -import ErrorSummary from '../'; +import { createRef } from 'react'; +import { ErrorSummary } from '..'; +import { renderClient, renderServer } from '#util/components'; describe('ErrorSummary', () => { it('matches snapshot', async () => { diff --git a/src/components/form-elements/error-summary/__tests__/__snapshots__/ErrorSummary.test.tsx.snap b/src/components/form-elements/error-summary/__tests__/__snapshots__/ErrorSummary.test.tsx.snap index 80b2b61f..666eb326 100644 --- a/src/components/form-elements/error-summary/__tests__/__snapshots__/ErrorSummary.test.tsx.snap +++ b/src/components/form-elements/error-summary/__tests__/__snapshots__/ErrorSummary.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`ErrorSummary ErrorSummary.List matches snapshot: ErrorSummary.List 1`] = `
    diff --git a/src/components/form-elements/error-summary/index.ts b/src/components/form-elements/error-summary/index.ts index 20fb46a1..49eaf1ac 100644 --- a/src/components/form-elements/error-summary/index.ts +++ b/src/components/form-elements/error-summary/index.ts @@ -1 +1 @@ -export { default } from './ErrorSummary'; +export * from './ErrorSummary.js'; diff --git a/src/components/form-elements/fieldset/Fieldset.tsx b/src/components/form-elements/fieldset/Fieldset.tsx index 3d3e0d3a..ee23b7e1 100644 --- a/src/components/form-elements/fieldset/Fieldset.tsx +++ b/src/components/form-elements/fieldset/Fieldset.tsx @@ -1,6 +1,6 @@ -import React, { ComponentPropsWithoutRef, FC } from 'react'; import classNames from 'classnames'; -import Legend from '../legend/Legend'; +import { type ComponentPropsWithoutRef, type FC } from 'react'; +import { Legend } from '#components/form-elements/legend/index.js'; export type FieldsetProps = ComponentPropsWithoutRef<'fieldset'>; @@ -18,6 +18,6 @@ const FieldsetComponent: FC = ({ children, className, ...rest }) FieldsetComponent.displayName = 'Fieldset'; -export default Object.assign(FieldsetComponent, { +export const Fieldset = Object.assign(FieldsetComponent, { Legend, }); diff --git a/src/components/form-elements/fieldset/__tests__/Fieldset.test.tsx b/src/components/form-elements/fieldset/__tests__/Fieldset.test.tsx index 220a91f1..e12ef428 100644 --- a/src/components/form-elements/fieldset/__tests__/Fieldset.test.tsx +++ b/src/components/form-elements/fieldset/__tests__/Fieldset.test.tsx @@ -1,7 +1,6 @@ -import React from 'react'; import { render } from '@testing-library/react'; -import Fieldset from '../'; -import TextInput from '@components/form-elements/text-input'; +import { Fieldset } from '..'; +import { TextInput } from '../..'; describe('Fieldset', () => { it('matches snapshot', () => { diff --git a/src/components/form-elements/fieldset/__tests__/__snapshots__/Fieldset.test.tsx.snap b/src/components/form-elements/fieldset/__tests__/__snapshots__/Fieldset.test.tsx.snap index 97e8f3fe..7b79d8e3 100644 --- a/src/components/form-elements/fieldset/__tests__/__snapshots__/Fieldset.test.tsx.snap +++ b/src/components/form-elements/fieldset/__tests__/__snapshots__/Fieldset.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`Fieldset matches snapshot: Fieldset 1`] = `
    diff --git a/src/components/form-elements/fieldset/index.ts b/src/components/form-elements/fieldset/index.ts index e5d92007..cc9bf42f 100644 --- a/src/components/form-elements/fieldset/index.ts +++ b/src/components/form-elements/fieldset/index.ts @@ -1 +1 @@ -export { default } from './Fieldset'; +export * from './Fieldset.js'; diff --git a/src/components/form-elements/form/Form.tsx b/src/components/form-elements/form/Form.tsx index 0bf2b67e..b438f77d 100644 --- a/src/components/form-elements/form/Form.tsx +++ b/src/components/form-elements/form/Form.tsx @@ -1,16 +1,14 @@ -import React, { ComponentPropsWithoutRef, FC } from 'react'; -import FormContext from './FormContext'; +import { type ComponentPropsWithoutRef, type FC } from 'react'; +import { FormContext } from './FormContext.js'; -type FormProps = ComponentPropsWithoutRef<'form'> & { +export type FormProps = ComponentPropsWithoutRef<'form'> & { disableErrorFromComponents?: boolean; }; -const FormComponent: FC = ({ disableErrorFromComponents, ...rest }) => ( +export const Form: FC = ({ disableErrorFromComponents, ...rest }) => (
    ); -FormComponent.displayName = 'Form'; - -export default FormComponent; +Form.displayName = 'Form'; diff --git a/src/components/form-elements/form/FormContext.ts b/src/components/form-elements/form/FormContext.ts index 68b12f70..6fce0b3d 100644 --- a/src/components/form-elements/form/FormContext.ts +++ b/src/components/form-elements/form/FormContext.ts @@ -1,13 +1,13 @@ +'use client'; + import { createContext, useContext } from 'react'; export interface IFormContext { disableErrorFromComponents: boolean; } -const FormContext = createContext({ +export const FormContext = createContext({ disableErrorFromComponents: false, }); export const useFormContext = (): IFormContext => useContext(FormContext); - -export default FormContext; diff --git a/src/components/form-elements/form/index.ts b/src/components/form-elements/form/index.ts index 84bc0aca..061c9d54 100644 --- a/src/components/form-elements/form/index.ts +++ b/src/components/form-elements/form/index.ts @@ -1 +1,2 @@ -export { default, useFormContext } from './FormContext'; +export * from './FormContext.js'; +export * from './Form.js'; diff --git a/src/components/form-elements/hint-text/HintText.tsx b/src/components/form-elements/hint-text/HintText.tsx index 34f1c5f2..918041b1 100644 --- a/src/components/form-elements/hint-text/HintText.tsx +++ b/src/components/form-elements/hint-text/HintText.tsx @@ -1,9 +1,9 @@ -import React, { ComponentPropsWithoutRef, FC } from 'react'; import classNames from 'classnames'; +import { type ComponentPropsWithoutRef, type FC } from 'react'; export type HintTextProps = ComponentPropsWithoutRef<'div'>; -const HintTextComponent: FC = ({ children, className, ...rest }) => { +export const HintText: FC = ({ children, className, ...rest }) => { if (!children) { return null; } @@ -15,6 +15,4 @@ const HintTextComponent: FC = ({ children, className, ...rest }) ); }; -HintTextComponent.displayName = 'HintText'; - -export default HintTextComponent; +HintText.displayName = 'HintText'; diff --git a/src/components/form-elements/hint-text/__tests__/Hint.test.tsx b/src/components/form-elements/hint-text/__tests__/Hint.test.tsx index f5787c9c..e58b3a62 100644 --- a/src/components/form-elements/hint-text/__tests__/Hint.test.tsx +++ b/src/components/form-elements/hint-text/__tests__/Hint.test.tsx @@ -1,6 +1,5 @@ -import React from 'react'; import { render } from '@testing-library/react'; -import HintText from '../'; +import { HintText } from '..'; describe('Hint', () => { it('matches snapshot', () => { diff --git a/src/components/form-elements/hint-text/__tests__/__snapshots__/Hint.test.tsx.snap b/src/components/form-elements/hint-text/__tests__/__snapshots__/Hint.test.tsx.snap index 17b56019..18045042 100644 --- a/src/components/form-elements/hint-text/__tests__/__snapshots__/Hint.test.tsx.snap +++ b/src/components/form-elements/hint-text/__tests__/__snapshots__/Hint.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`Hint matches snapshot: Hint 1`] = `
    diff --git a/src/components/form-elements/hint-text/index.ts b/src/components/form-elements/hint-text/index.ts index 708441db..9388f600 100644 --- a/src/components/form-elements/hint-text/index.ts +++ b/src/components/form-elements/hint-text/index.ts @@ -1 +1 @@ -export { default } from './HintText'; +export * from './HintText.js'; diff --git a/src/components/form-elements/index.ts b/src/components/form-elements/index.ts new file mode 100644 index 00000000..23b427ff --- /dev/null +++ b/src/components/form-elements/index.ts @@ -0,0 +1,15 @@ +export * from './button/index.js'; +export * from './character-count/index.js'; +export * from './checkboxes/index.js'; +export * from './date-input/index.js'; +export * from './error-message/index.js'; +export * from './error-summary/index.js'; +export * from './fieldset/index.js'; +export * from './form/index.js'; +export * from './hint-text/index.js'; +export * from './label/index.js'; +export * from './legend/index.js'; +export * from './radios/index.js'; +export * from './select/index.js'; +export * from './text-input/index.js'; +export * from './textarea/index.js'; diff --git a/src/components/form-elements/label/Label.tsx b/src/components/form-elements/label/Label.tsx index 734503c2..13b0c242 100644 --- a/src/components/form-elements/label/Label.tsx +++ b/src/components/form-elements/label/Label.tsx @@ -1,13 +1,13 @@ -import React, { ComponentPropsWithoutRef, FC } from 'react'; import classNames from 'classnames'; -import { NHSUKSize } from '@util/types/NHSUKTypes'; +import { type ComponentPropsWithoutRef, type FC } from 'react'; +import { type NHSUKSize } from '#util/types/NHSUKTypes.js'; export interface LabelProps extends ComponentPropsWithoutRef<'label'> { isPageHeading?: boolean; size?: NHSUKSize; } -const Label: FC> = ({ className, size, ...rest }) => ( +const LabelComponent: FC> = ({ className, size, ...rest }) => ( // eslint-disable-next-line jsx-a11y/label-has-associated-control