diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 61d6e8031..000000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -jest.config.js -.eslintrc.cjs -rollup.config.js -dist \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index 8a2a92543..000000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,43 +0,0 @@ -module.exports = { - parserOptions: { - projectService: true, - tsconfigRootDir: __dirname, - }, - env: { - browser: true, - jest: true, - }, - settings: { - 'import/resolver': { - typescript: {}, - }, - react: { - version: 'detect', - }, - }, - extends: [ - 'plugin:react/recommended', - 'plugin:react/jsx-runtime', - 'plugin:@typescript-eslint/recommended', - 'prettier', - 'plugin:import/errors', - 'plugin:import/warnings', - 'plugin:import/typescript', - 'plugin:jsx-a11y/recommended', - 'plugin:react-hooks/recommended', - ], - rules: { - 'import/no-unresolved': 'off', - '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 aaf9601b2..0079701cb 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 c3d4732f7..da2ef3182 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 000000000..3741fba3f --- /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 ca8527e0d..feb652799 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/manager.ts b/.storybook/manager.ts index 9771ffb7a..948be460e 100644 --- a/.storybook/manager.ts +++ b/.storybook/manager.ts @@ -12,7 +12,7 @@ const sentenceCase = (name = '') => { 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 4f720fd03..ddf28734d 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -19,4 +19,5 @@ const preview: Preview = { }, }, }; + export default preview; diff --git a/.storybook/theme.ts b/.storybook/theme.ts index 5d3ea6517..2c4412575 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 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 667ff68ee..340494c77 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 7af902845..6c36df990 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1,2 +1,22 @@ nodeLinker: node-modules -npmRegistryServer: https://registry.yarnpkg.com + +npmRegistryServer: "https://registry.yarnpkg.com" + +packageExtensions: + "@storybook/addon-docs@*": + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + + "@storybook/addon-essentials@*": + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + + "@storybook/core@*": + peerDependencies: + storybook: "*" + + "@storybook/react-vite@*": + peerDependencies: + typescript: "*" diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..2d8cf8cfd --- /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/package.json b/package.json index 0377af278..fb831aa19 100644 --- a/package.json +++ b/package.json @@ -55,12 +55,15 @@ "build": "yarn cleanup && rollup -c", "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}'", + "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", "build-storybook": "storybook build", - "prepublishOnly": "yarn lint:ci && yarn test:ci && yarn storybook --smoke-test" + "prepublishOnly": "yarn lint && yarn test && yarn storybook --smoke-test" }, "license": "MIT", "devDependencies": { @@ -68,64 +71,67 @@ "@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-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", + "@storybook/addon-essentials": "^8.6.14", + "@storybook/addon-links": "^8.6.14", + "@storybook/blocks": "^8.6.14", + "@storybook/manager-api": "^8.6.14", + "@storybook/react": "^8.6.14", + "@storybook/react-vite": "^8.6.14", + "@storybook/theming": "^8.6.14", + "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^15.0.7", + "@testing-library/react": "^16.3.0", + "@types/eslint": "^9.6.1", "@types/jest": "^30.0.0", "@types/jest-axe": "^3.5.9", + "@types/lodash": "^4.17.20", "@types/node": "^24.6.2", - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", - "@typescript-eslint/eslint-plugin": "^7.1.0", - "@typescript-eslint/parser": "^7.1.0", + "@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", "chromatic": "^6.17.3", "classnames": "^2.5.1", - "eslint": "^8.57.0", - "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", + "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", + "prettier": "^3.6.2", + "react": "^19.2.0", + "react-dom": "^19.2.0", "rollup": "^4.52.4", "rollup-plugin-preserve-directives": "^0.4.0", "sass": "^1.53.0", - "storybook": "^8.0.5", + "storybook": "^8.6.14", "tslib": "^2.8.1", - "typescript": "5.3.3", + "typescript": "^5.9.3", + "typescript-eslint": "^8.45.0", "vite": "^4.5.3", "vite-tsconfig-paths": "^4.3.2" }, "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/src/components/content-presentation/do-and-dont-list/DoAndDontList.tsx b/src/components/content-presentation/do-and-dont-list/DoAndDontList.tsx index 8392f8459..7c2a12db4 100644 --- a/src/components/content-presentation/do-and-dont-list/DoAndDontList.tsx +++ b/src/components/content-presentation/do-and-dont-list/DoAndDontList.tsx @@ -1,3 +1,5 @@ +'use client'; + import classNames from 'classnames'; import { createContext, diff --git a/src/components/content-presentation/table/Table.tsx b/src/components/content-presentation/table/Table.tsx index 311227019..07bad0aeb 100644 --- a/src/components/content-presentation/table/Table.tsx +++ b/src/components/content-presentation/table/Table.tsx @@ -1,3 +1,5 @@ +'use client'; + import classNames from 'classnames'; import { forwardRef, @@ -45,7 +47,7 @@ const TableComponent = forwardRef((props, forwarde responsive, setHeadings, }; - }, [responsive, headings, setHeadings]); + }, [firstCellIsHeader, headings, responsive, setHeadings]); return ( diff --git a/src/components/content-presentation/table/TableContext.ts b/src/components/content-presentation/table/TableContext.ts index 0b15b0945..779d58c05 100644 --- a/src/components/content-presentation/table/TableContext.ts +++ b/src/components/content-presentation/table/TableContext.ts @@ -1,3 +1,5 @@ +'use client'; + import { createContext, type ReactNode } from 'react'; export interface ITableContext { @@ -8,7 +10,6 @@ export interface ITableContext { } export const TableContext = createContext({ - /* eslint-disable @typescript-eslint/no-empty-function */ firstCellIsHeader: false, headings: [], responsive: false, diff --git a/src/components/content-presentation/table/TableSectionContext.ts b/src/components/content-presentation/table/TableSectionContext.ts index 8ae402378..86b80b5cf 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 { diff --git a/src/components/content-presentation/table/components/TableCell.tsx b/src/components/content-presentation/table/components/TableCell.tsx index 89d8dfb11..07288afc0 100644 --- a/src/components/content-presentation/table/components/TableCell.tsx +++ b/src/components/content-presentation/table/components/TableCell.tsx @@ -1,3 +1,5 @@ +'use client'; + import classNames from 'classnames'; import { useContext, type ComponentPropsWithoutRef, type FC } from 'react'; import { TableContext, type ITableContext } from '../TableContext.js'; diff --git a/src/components/content-presentation/table/components/TableHead.tsx b/src/components/content-presentation/table/components/TableHead.tsx index 0f5f0d256..cc5b7d21f 100644 --- a/src/components/content-presentation/table/components/TableHead.tsx +++ b/src/components/content-presentation/table/components/TableHead.tsx @@ -1,3 +1,5 @@ +'use client'; + import classNames from 'classnames'; import { useContext, type ComponentPropsWithoutRef, type FC } from 'react'; import { TableContext, type ITableContext } from '../TableContext.js'; diff --git a/src/components/content-presentation/table/components/TableRow.tsx b/src/components/content-presentation/table/components/TableRow.tsx index dd35abb65..027b30c41 100644 --- a/src/components/content-presentation/table/components/TableRow.tsx +++ b/src/components/content-presentation/table/components/TableRow.tsx @@ -1,3 +1,5 @@ +'use client'; + import classNames from 'classnames'; import { Children, @@ -19,7 +21,7 @@ export const TableRow: FC> = ({ children, classNa 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) 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 290356d5a..90fe20429 100644 --- a/src/components/content-presentation/table/components/__tests__/TableBody.test.tsx +++ b/src/components/content-presentation/table/components/__tests__/TableBody.test.tsx @@ -1,5 +1,4 @@ import { render } from '@testing-library/react'; -import { useContext } from 'react'; import { TableBody } from '..'; import { Table, TableSection, TableSectionContext } from '../..'; @@ -15,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__/TableCell.test.tsx b/src/components/content-presentation/table/components/__tests__/TableCell.test.tsx index 2fc8ced7d..284950430 100644 --- a/src/components/content-presentation/table/components/__tests__/TableCell.test.tsx +++ b/src/components/content-presentation/table/components/__tests__/TableCell.test.tsx @@ -29,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__/TableHead.test.tsx b/src/components/content-presentation/table/components/__tests__/TableHead.test.tsx index 342291f4b..5897dfc8d 100644 --- a/src/components/content-presentation/table/components/__tests__/TableHead.test.tsx +++ b/src/components/content-presentation/table/components/__tests__/TableHead.test.tsx @@ -1,5 +1,4 @@ import { render } from '@testing-library/react'; -import { useContext } from 'react'; import { TableHead } from '..'; import { Table, TableSection, TableSectionContext } from '../..'; @@ -15,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/tabs/Tabs.tsx b/src/components/content-presentation/tabs/Tabs.tsx index 68c62b632..9fbcaf56a 100644 --- a/src/components/content-presentation/tabs/Tabs.tsx +++ b/src/components/content-presentation/tabs/Tabs.tsx @@ -1,3 +1,5 @@ +'use client'; + import classNames from 'classnames'; import { type Tabs as TabsModule } from 'nhsuk-frontend'; import { diff --git a/src/components/form-elements/button/Button.tsx b/src/components/form-elements/button/Button.tsx index 98ac2cd44..56d07bbec 100644 --- a/src/components/form-elements/button/Button.tsx +++ b/src/components/form-elements/button/Button.tsx @@ -1,3 +1,5 @@ +'use client'; + import classNames from 'classnames'; import { type Button as ButtonModule } from 'nhsuk-frontend'; import { diff --git a/src/components/form-elements/character-count/CharacterCount.tsx b/src/components/form-elements/character-count/CharacterCount.tsx index edba9bdbd..84779eb13 100644 --- a/src/components/form-elements/character-count/CharacterCount.tsx +++ b/src/components/form-elements/character-count/CharacterCount.tsx @@ -1,3 +1,5 @@ +'use client'; + import classNames from 'classnames'; import { type CharacterCount as CharacterCountModule } from 'nhsuk-frontend'; import { createRef, forwardRef, useEffect, useState, type ComponentPropsWithoutRef } from 'react'; @@ -34,12 +36,12 @@ export const CharacterCount = forwardRef diff --git a/src/components/form-elements/checkboxes/Checkboxes.tsx b/src/components/form-elements/checkboxes/Checkboxes.tsx index 16ddd3d0c..c83c98061 100644 --- a/src/components/form-elements/checkboxes/Checkboxes.tsx +++ b/src/components/form-elements/checkboxes/Checkboxes.tsx @@ -1,3 +1,5 @@ +'use client'; + import classNames from 'classnames'; import { type Checkboxes as CheckboxesModule } from 'nhsuk-frontend'; import { createRef, forwardRef, useEffect, useState, type ComponentPropsWithoutRef } from 'react'; diff --git a/src/components/form-elements/checkboxes/CheckboxesContext.ts b/src/components/form-elements/checkboxes/CheckboxesContext.ts index 207cb556a..06ce7037a 100644 --- a/src/components/form-elements/checkboxes/CheckboxesContext.ts +++ b/src/components/form-elements/checkboxes/CheckboxesContext.ts @@ -1,3 +1,5 @@ +'use client'; + import { createContext } from 'react'; export interface ICheckboxesContext { @@ -8,7 +10,6 @@ export interface ICheckboxesContext { } export const CheckboxesContext = createContext({ - /* eslint-disable @typescript-eslint/no-empty-function */ name: '', getBoxId: () => undefined, leaseReference: () => '', diff --git a/src/components/form-elements/checkboxes/components/Item.tsx b/src/components/form-elements/checkboxes/components/Item.tsx index 6cc377100..776140127 100644 --- a/src/components/form-elements/checkboxes/components/Item.tsx +++ b/src/components/form-elements/checkboxes/components/Item.tsx @@ -1,3 +1,5 @@ +'use client'; + import classNames from 'classnames'; import { forwardRef, @@ -42,15 +44,15 @@ export const CheckboxesItem = forwardRef( const { getBoxId, name, leaseReference, unleaseReference } = useContext(CheckboxesContext); - const [boxReference] = useState(leaseReference()); - const inputID = id || getBoxId(boxReference); + const [checkboxReference] = useState(leaseReference()); + const inputID = id || getBoxId(checkboxReference); const shouldShowConditional = !!(checked || defaultChecked); const { className: labelClassName, ...restLabelProps } = labelProps || {}; const { className: hintClassName, ...restHintProps } = hintProps || {}; const { className: conditionalClassName, ...restConditionalProps } = conditionalProps || {}; - useEffect(() => () => unleaseReference(boxReference), []); + useEffect(() => () => unleaseReference(checkboxReference)); const inputProps: HTMLAttributesWithData = rest; diff --git a/src/components/form-elements/date-input/DateInput.tsx b/src/components/form-elements/date-input/DateInput.tsx index e4ae22983..03f936271 100644 --- a/src/components/form-elements/date-input/DateInput.tsx +++ b/src/components/form-elements/date-input/DateInput.tsx @@ -1,8 +1,9 @@ +'use client'; + import classNames from 'classnames'; import { createRef, forwardRef, - useEffect, useState, type ChangeEvent, type ComponentPropsWithoutRef, @@ -50,16 +51,6 @@ const DateInputComponent = forwardRef( year: value?.year ?? '', }); - useEffect(() => { - const newState = { ...internalDate }; - const { day, month, year } = value ?? {}; - if (day && day !== internalDate.day) newState.day = day; - if (month && month !== internalDate.month) newState.month = month; - if (year && year !== internalDate.year) newState.year = year; - - return setInternalDate(newState); - }, [value]); - const handleChange = (inputType: InputType, event: ChangeEvent): void => { event.stopPropagation(); @@ -85,7 +76,6 @@ const DateInputComponent = forwardRef( inputType="dateinput" {...rest} > - {/* eslint-disable-next-line @typescript-eslint/no-unused-vars */} {({ className, name, id, error, ...restRenderProps }) => { const contextValue: IDateInputContext = { id, diff --git a/src/components/form-elements/date-input/DateInputContext.ts b/src/components/form-elements/date-input/DateInputContext.ts index 50b053772..a520a1083 100644 --- a/src/components/form-elements/date-input/DateInputContext.ts +++ b/src/components/form-elements/date-input/DateInputContext.ts @@ -1,3 +1,5 @@ +'use client'; + import { createContext, type ChangeEvent } from 'react'; export type IDateInputContext = { @@ -10,7 +12,6 @@ export type IDateInputContext = { }; export const DateInputContext = createContext({ - /* eslint-disable @typescript-eslint/no-empty-function */ id: '', name: '', handleChange: () => {}, diff --git a/src/components/form-elements/date-input/__tests__/__snapshots__/DateInput.test.tsx.snap b/src/components/form-elements/date-input/__tests__/__snapshots__/DateInput.test.tsx.snap index 383fb133f..6a871bd99 100644 --- a/src/components/form-elements/date-input/__tests__/__snapshots__/DateInput.test.tsx.snap +++ b/src/components/form-elements/date-input/__tests__/__snapshots__/DateInput.test.tsx.snap @@ -44,7 +44,6 @@ exports[`DateInput matches snapshot (via server): client 1`] = ` inputmode="numeric" name="date-input-day" type="text" - value="" /> @@ -67,7 +66,6 @@ exports[`DateInput matches snapshot (via server): client 1`] = ` inputmode="numeric" name="date-input-month" type="text" - value="" /> @@ -90,7 +88,6 @@ exports[`DateInput matches snapshot (via server): client 1`] = ` inputmode="numeric" name="date-input-year" type="text" - value="" /> @@ -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 0a127d595..176436d67 100644 --- a/src/components/form-elements/date-input/components/IndividualDateInputs.tsx +++ b/src/components/form-elements/date-input/components/IndividualDateInputs.tsx @@ -1,3 +1,5 @@ +'use client'; + import classNames from 'classnames'; import { forwardRef, useContext, type ChangeEvent, type ComponentPropsWithoutRef } from 'react'; import { DateInputContext, type IDateInputContext } from '../DateInputContext.js'; diff --git a/src/components/form-elements/error-summary/ErrorSummary.tsx b/src/components/form-elements/error-summary/ErrorSummary.tsx index ed248476b..0ba53b5d5 100644 --- a/src/components/form-elements/error-summary/ErrorSummary.tsx +++ b/src/components/form-elements/error-summary/ErrorSummary.tsx @@ -1,3 +1,5 @@ +'use client'; + import classNames from 'classnames'; import { type ErrorSummary as ErrorSummaryModule } from 'nhsuk-frontend'; import { diff --git a/src/components/form-elements/form/FormContext.ts b/src/components/form-elements/form/FormContext.ts index a35909c0e..6fce0b3da 100644 --- a/src/components/form-elements/form/FormContext.ts +++ b/src/components/form-elements/form/FormContext.ts @@ -1,3 +1,5 @@ +'use client'; + import { createContext, useContext } from 'react'; export interface IFormContext { diff --git a/src/components/form-elements/radios/Radios.tsx b/src/components/form-elements/radios/Radios.tsx index d5405f04d..9b30a1765 100644 --- a/src/components/form-elements/radios/Radios.tsx +++ b/src/components/form-elements/radios/Radios.tsx @@ -1,3 +1,5 @@ +'use client'; + import classNames from 'classnames'; import { type Radios as RadiosModule } from 'nhsuk-frontend'; import { createRef, forwardRef, useEffect, useState, type ComponentPropsWithoutRef } from 'react'; diff --git a/src/components/form-elements/radios/RadiosContext.ts b/src/components/form-elements/radios/RadiosContext.ts index fb1353e52..c097c2ca4 100644 --- a/src/components/form-elements/radios/RadiosContext.ts +++ b/src/components/form-elements/radios/RadiosContext.ts @@ -1,3 +1,5 @@ +'use client'; + import { createContext } from 'react'; export type IRadiosContext = { @@ -10,7 +12,6 @@ export type IRadiosContext = { }; export const RadiosContext = createContext({ - /* eslint-disable @typescript-eslint/no-empty-function */ name: '', selectedRadio: '', getRadioId: () => '', diff --git a/src/components/form-elements/radios/components/Item.tsx b/src/components/form-elements/radios/components/Item.tsx index 03ac417c9..5c7a3fbca 100644 --- a/src/components/form-elements/radios/components/Item.tsx +++ b/src/components/form-elements/radios/components/Item.tsx @@ -1,3 +1,5 @@ +'use client'; + import classNames from 'classnames'; import { forwardRef, @@ -48,11 +50,11 @@ export const RadiosItem = forwardRef((props, useEffect(() => { if (defaultChecked) setSelected(radioReference); - }, []); + }, [defaultChecked, setSelected, radioReference]); useEffect(() => { if (checked) setSelected(radioReference); - }, [checked]); + }, [checked, setSelected, radioReference]); return ( <> diff --git a/src/components/navigation/card/CardContext.ts b/src/components/navigation/card/CardContext.ts index 7c571068b..17caf2e37 100644 --- a/src/components/navigation/card/CardContext.ts +++ b/src/components/navigation/card/CardContext.ts @@ -1,3 +1,5 @@ +'use client'; + import { createContext } from 'react'; import { type CardType } from '#util/types/index.js'; diff --git a/src/components/navigation/card/__tests__/__snapshots__/Card.test.tsx.snap b/src/components/navigation/card/__tests__/__snapshots__/Card.test.tsx.snap index 8ba0ea04f..a4ab772b3 100644 --- a/src/components/navigation/card/__tests__/__snapshots__/Card.test.tsx.snap +++ b/src/components/navigation/card/__tests__/__snapshots__/Card.test.tsx.snap @@ -148,6 +148,11 @@ exports[`Card Care card variant urgent matches the snapshot 1`] = ` exports[`Card matches snapshot (via server): client 1`] = `
+
@@ -187,6 +192,11 @@ exports[`Card matches snapshot (via server): client 1`] = ` exports[`Card matches snapshot (via server): server 1`] = `
+
diff --git a/src/components/navigation/card/components/CardContent.tsx b/src/components/navigation/card/components/CardContent.tsx index ed491c6af..9332921fe 100644 --- a/src/components/navigation/card/components/CardContent.tsx +++ b/src/components/navigation/card/components/CardContent.tsx @@ -1,3 +1,5 @@ +'use client'; + import classNames from 'classnames'; import { forwardRef, useContext, type ComponentPropsWithoutRef } from 'react'; import { CardContext } from '../CardContext.js'; diff --git a/src/components/navigation/card/components/CardHeading.tsx b/src/components/navigation/card/components/CardHeading.tsx index 1f012079a..93fd49a08 100644 --- a/src/components/navigation/card/components/CardHeading.tsx +++ b/src/components/navigation/card/components/CardHeading.tsx @@ -1,3 +1,5 @@ +'use client'; + import classNames from 'classnames'; import { forwardRef, useContext } from 'react'; import { CardContext } from '../CardContext.js'; diff --git a/src/components/navigation/header/Header.tsx b/src/components/navigation/header/Header.tsx index cd2232db9..b6dc39e37 100644 --- a/src/components/navigation/header/Header.tsx +++ b/src/components/navigation/header/Header.tsx @@ -1,3 +1,5 @@ +'use client'; + import classNames from 'classnames'; import { type Header as HeaderModule } from 'nhsuk-frontend'; import { @@ -106,7 +108,7 @@ const HeaderComponent = forwardRef((props, forwardedRe setServiceProps, setOrganisationProps, }; - }, [logoProps, serviceProps, organisationProps]); + }, [logoProps, serviceProps, organisationProps, menuOpen]); const items = Children.toArray(children); const childLogo = items.find((child) => childIsOfComponentType(child, Logo)); diff --git a/src/components/navigation/header/HeaderContext.ts b/src/components/navigation/header/HeaderContext.ts index 0c7f50929..72a3b01d8 100644 --- a/src/components/navigation/header/HeaderContext.ts +++ b/src/components/navigation/header/HeaderContext.ts @@ -1,10 +1,12 @@ +'use client'; + import { createContext, type Dispatch, type SetStateAction } from 'react'; export interface IHeaderContext { logoProps?: { - href?: string; - src?: string; - alt?: string; + 'href'?: string; + 'src'?: string; + 'alt'?: string; 'aria-label'?: string; }; serviceProps?: { @@ -24,7 +26,6 @@ export interface IHeaderContext { } export const HeaderContext = createContext({ - /* eslint-disable @typescript-eslint/no-empty-function */ logoProps: undefined, serviceProps: undefined, organisationProps: undefined, diff --git a/src/components/navigation/header/components/Logo.tsx b/src/components/navigation/header/components/Logo.tsx index f682e6bcc..b9b508f80 100644 --- a/src/components/navigation/header/components/Logo.tsx +++ b/src/components/navigation/header/components/Logo.tsx @@ -1,3 +1,5 @@ +'use client'; + import { useContext, useEffect, type FC } from 'react'; import { HeaderContext, type IHeaderContext } from '../HeaderContext.js'; @@ -14,7 +16,7 @@ export const Logo: FC = (logo) => { setLogoProps(logo); return () => setLogoProps(undefined); - }, [logo]); + }, [logo, setLogoProps]); const { alt = 'NHS' } = logo; diff --git a/src/components/navigation/header/components/Navigation.tsx b/src/components/navigation/header/components/Navigation.tsx index 22b518154..9900d26a7 100644 --- a/src/components/navigation/header/components/Navigation.tsx +++ b/src/components/navigation/header/components/Navigation.tsx @@ -1,3 +1,5 @@ +'use client'; + import classNames from 'classnames'; import { useContext, useEffect, type ComponentPropsWithoutRef, type FC } from 'react'; import { HeaderContext, type IHeaderContext } from '../HeaderContext.js'; @@ -26,7 +28,7 @@ export const Navigation: FC = ({ setMenuOpen(open); return () => setMenuOpen(false); - }, [open]); + }, [open, setMenuOpen]); return (