From bf21c46e52cee76f6663af6449ceb02bc3a06679 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Fri, 1 Aug 2025 22:31:30 +0100 Subject: [PATCH 01/31] RouterTab: update file type to tsx --no-verify --- client/common/{RouterTab.jsx => RouterTab.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/common/{RouterTab.jsx => RouterTab.tsx} (100%) diff --git a/client/common/RouterTab.jsx b/client/common/RouterTab.tsx similarity index 100% rename from client/common/RouterTab.jsx rename to client/common/RouterTab.tsx From 7ff4aa32c257b378b235d3c4a98ec62bc78670f9 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Fri, 1 Aug 2025 22:45:52 +0100 Subject: [PATCH 02/31] RouterTab.tsx: add types and add router dom types and unit test --- client/common/RouterTab.test.tsx | 32 ++++++++++++++++++ client/common/RouterTab.tsx | 14 ++++---- package-lock.json | 58 ++++++++++++++++++++++++++++++++ package.json | 1 + 4 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 client/common/RouterTab.test.tsx diff --git a/client/common/RouterTab.test.tsx b/client/common/RouterTab.test.tsx new file mode 100644 index 0000000000..986a9b9de6 --- /dev/null +++ b/client/common/RouterTab.test.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import Tab from './RouterTab'; + +describe('Tab', () => { + it('renders a NavLink with correct text and link', () => { + render( + + Dashboard + + ); + + const linkElement = screen.getByText('Dashboard'); + expect(linkElement).toBeInTheDocument(); + expect(linkElement.getAttribute('href')).toBe('/dashboard'); + }); + + it('includes the dashboard-header class names', () => { + const { container } = render( + + Settings + + ); + + const listItem = container.querySelector('li'); + const link = container.querySelector('a'); + + expect(listItem).toHaveClass('dashboard-header__tab'); + expect(link).toHaveClass('dashboard-header__tab__title'); + }); +}); diff --git a/client/common/RouterTab.tsx b/client/common/RouterTab.tsx index d08c839855..bb97591f6c 100644 --- a/client/common/RouterTab.tsx +++ b/client/common/RouterTab.tsx @@ -1,11 +1,14 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +import React, { ReactNode } from 'react'; import { NavLink } from 'react-router-dom'; +type TabProps = { + children: ReactNode, + to: string +}; /** * Wraps the react-router `NavLink` with dashboard-header__tab styling. */ -const Tab = ({ children, to }) => ( +const Tab = ({ children, to }: TabProps) => (
  • (
  • ); -Tab.propTypes = { - children: PropTypes.string.isRequired, - to: PropTypes.string.isRequired -}; - export default Tab; diff --git a/package-lock.json b/package-lock.json index aa67ab3b9d..f45f72ed0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -158,6 +158,7 @@ "@types/node": "^16.18.126", "@types/react": "^16.14.0", "@types/react-dom": "^16.9.25", + "@types/react-router-dom": "^5.3.3", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "babel-core": "^7.0.0-bridge.0", @@ -14131,6 +14132,13 @@ "@types/unist": "*" } }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", @@ -14368,6 +14376,29 @@ "redux": "^4.0.0" } }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, "node_modules/@types/react/node_modules/csstype": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz", @@ -50465,6 +50496,12 @@ "@types/unist": "*" } }, + "@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "dev": true + }, "@types/hoist-non-react-statics": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", @@ -50706,6 +50743,27 @@ "redux": "^4.0.0" } }, + "@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "dev": true, + "requires": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "dev": true, + "requires": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, "@types/redux-devtools-themes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/redux-devtools-themes/-/redux-devtools-themes-1.0.0.tgz", diff --git a/package.json b/package.json index ccf2842f22..e2a61fe2f3 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,7 @@ "@types/node": "^16.18.126", "@types/react": "^16.14.0", "@types/react-dom": "^16.9.25", + "@types/react-router-dom": "^5.3.3", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "babel-core": "^7.0.0-bridge.0", From 551ded1f3182e2d911b29f3d443d64c51e6f1589 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Fri, 1 Aug 2025 23:06:12 +0100 Subject: [PATCH 03/31] IconButton: update to tsx --no-verify --- client/common/{IconButton.jsx => IconButton.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/common/{IconButton.jsx => IconButton.tsx} (100%) diff --git a/client/common/IconButton.jsx b/client/common/IconButton.tsx similarity index 100% rename from client/common/IconButton.jsx rename to client/common/IconButton.tsx From 835b464045e433c858dd19e6849cacd612517f52 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Fri, 1 Aug 2025 23:45:47 +0100 Subject: [PATCH 04/31] add types to IconButton.tsx --no-verify due to Icon not migrated yet --- client/common/IconButton.tsx | 25 ++++++++++++----------- package-lock.json | 39 ++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/client/common/IconButton.tsx b/client/common/IconButton.tsx index 8cf732f91d..e84d2f0cc5 100644 --- a/client/common/IconButton.tsx +++ b/client/common/IconButton.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { ComponentType } from 'react'; import styled from 'styled-components'; import Button from './Button'; import { remSize } from '../theme'; @@ -12,8 +11,18 @@ const ButtonWrapper = styled(Button)` } `; -const IconButton = (props) => { - const { icon, ...otherProps } = props; +type IconProps = { + 'aria-label'?: string +}; + +type IconButtonProps = Omit< + React.ComponentProps, + 'iconBefore' | 'iconOnly' | 'display' | 'focusable' +> & { + icon?: ComponentType | null +}; + +const IconButton = ({ icon = null, ...otherProps }: IconButtonProps) => { const Icon = icon; return ( @@ -27,12 +36,4 @@ const IconButton = (props) => { ); }; -IconButton.propTypes = { - icon: PropTypes.func -}; - -IconButton.defaultProps = { - icon: null -}; - export default IconButton; diff --git a/package-lock.json b/package-lock.json index f45f72ed0a..2cab375e99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -159,6 +159,7 @@ "@types/react": "^16.14.0", "@types/react-dom": "^16.9.25", "@types/react-router-dom": "^5.3.3", + "@types/styled-components": "^5.1.34", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "babel-core": "^7.0.0-bridge.0", @@ -14465,6 +14466,25 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/styled-components": { + "version": "5.1.34", + "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.34.tgz", + "integrity": "sha512-mmiVvwpYklFIv9E8qfxuPyIt/OuyIrn6gMOAMOFUO3WJfSrSE+sGUoa4PiZj77Ut7bKZpaa6o1fBKS/4TOEvnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hoist-non-react-statics": "*", + "@types/react": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/styled-components/node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/testing-library__jest-dom": { "version": "5.14.0", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.0.tgz", @@ -50825,6 +50845,25 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "@types/styled-components": { + "version": "5.1.34", + "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.34.tgz", + "integrity": "sha512-mmiVvwpYklFIv9E8qfxuPyIt/OuyIrn6gMOAMOFUO3WJfSrSE+sGUoa4PiZj77Ut7bKZpaa6o1fBKS/4TOEvnA==", + "dev": true, + "requires": { + "@types/hoist-non-react-statics": "*", + "@types/react": "*", + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + } + } + }, "@types/testing-library__jest-dom": { "version": "5.14.0", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.0.tgz", diff --git a/package.json b/package.json index e2a61fe2f3..3067280481 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "@types/react": "^16.14.0", "@types/react-dom": "^16.9.25", "@types/react-router-dom": "^5.3.3", + "@types/styled-components": "^5.1.34", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "babel-core": "^7.0.0-bridge.0", From 178b905a2b8810c79f13712364845b43ebe3840a Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sat, 2 Aug 2025 00:07:20 +0100 Subject: [PATCH 05/31] IconButton.tsx: add unit test and fix icon resolve --- .eslintrc | 3 ++- client/common/IconButton.test.tsx | 27 +++++++++++++++++++++++++++ client/common/IconButton.tsx | 22 +++++++++------------- 3 files changed, 38 insertions(+), 14 deletions(-) create mode 100644 client/common/IconButton.test.tsx diff --git a/.eslintrc b/.eslintrc index 0c9597ce98..0b800e2da2 100644 --- a/.eslintrc +++ b/.eslintrc @@ -131,7 +131,8 @@ "rules": { "no-use-before-define": "off", "import/no-extraneous-dependencies": "off", - "no-unused-vars": "off" + "no-unused-vars": "off", + "react/require-default-props": "off" } }, { diff --git a/client/common/IconButton.test.tsx b/client/common/IconButton.test.tsx new file mode 100644 index 0000000000..811a795184 --- /dev/null +++ b/client/common/IconButton.test.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { render, screen } from '../test-utils'; +import IconButton from './IconButton'; + +const MockIcon = (props: React.SVGProps) => ( + +); + +describe('IconButton', () => { + test('renders with an icon', () => { + render(); + expect(screen.getByTestId('mock-icon')).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'test button' }) + ).toBeInTheDocument(); + }); + + test('renders without an icon', () => { + render(); + expect(screen.queryByTestId('mock-icon')).not.toBeInTheDocument(); + }); + + test('passes other props to the button', () => { + render(); + expect(screen.getByRole('button')).toHaveAttribute('id', 'my-button'); + }); +}); diff --git a/client/common/IconButton.tsx b/client/common/IconButton.tsx index e84d2f0cc5..5e36ea472e 100644 --- a/client/common/IconButton.tsx +++ b/client/common/IconButton.tsx @@ -22,18 +22,14 @@ type IconButtonProps = Omit< icon?: ComponentType | null }; -const IconButton = ({ icon = null, ...otherProps }: IconButtonProps) => { - const Icon = icon; - - return ( - } - iconOnly - display={Button.displays.inline} - focusable="false" - {...otherProps} - /> - ); -}; +const IconButton = ({ icon: Icon, ...otherProps }: IconButtonProps) => ( + : undefined} + iconOnly + display={Button.displays.inline} + focusable="false" + {...otherProps} + /> +); export default IconButton; From adc04dd10e035f94e4430363be7af1f27d775c91 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sat, 2 Aug 2025 00:19:42 +0100 Subject: [PATCH 06/31] update to use test utils --- client/common/RouterTab.test.tsx | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/client/common/RouterTab.test.tsx b/client/common/RouterTab.test.tsx index 986a9b9de6..559529f4d2 100644 --- a/client/common/RouterTab.test.tsx +++ b/client/common/RouterTab.test.tsx @@ -1,15 +1,10 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; +import { render, screen } from '../test-utils'; import Tab from './RouterTab'; describe('Tab', () => { it('renders a NavLink with correct text and link', () => { - render( - - Dashboard - - ); + render(Dashboard); const linkElement = screen.getByText('Dashboard'); expect(linkElement).toBeInTheDocument(); @@ -17,11 +12,7 @@ describe('Tab', () => { }); it('includes the dashboard-header class names', () => { - const { container } = render( - - Settings - - ); + const { container } = render(Settings); const listItem = container.querySelector('li'); const link = container.querySelector('a'); From b60cfdf68a01d19d91f0f72b3106502f750cb88d Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sat, 2 Aug 2025 09:21:16 +0100 Subject: [PATCH 07/31] ButtonOrLink & test: update to ts, no-verify --- client/common/{ButtonOrLink.test.jsx => ButtonOrLink.test.tsx} | 0 client/common/{ButtonOrLink.jsx => ButtonOrLink.tsx} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename client/common/{ButtonOrLink.test.jsx => ButtonOrLink.test.tsx} (100%) rename client/common/{ButtonOrLink.jsx => ButtonOrLink.tsx} (100%) diff --git a/client/common/ButtonOrLink.test.jsx b/client/common/ButtonOrLink.test.tsx similarity index 100% rename from client/common/ButtonOrLink.test.jsx rename to client/common/ButtonOrLink.test.tsx diff --git a/client/common/ButtonOrLink.jsx b/client/common/ButtonOrLink.tsx similarity index 100% rename from client/common/ButtonOrLink.jsx rename to client/common/ButtonOrLink.tsx From 916819dc61a5a0ceaba761e7ba32d97ed30d57dc Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sat, 2 Aug 2025 10:05:49 +0100 Subject: [PATCH 08/31] ButtonOrLink: add types --- client/common/ButtonOrLink.tsx | 65 +++++++++++++++++----------------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/client/common/ButtonOrLink.tsx b/client/common/ButtonOrLink.tsx index f759aa8ffb..5ce49f8ea4 100644 --- a/client/common/ButtonOrLink.tsx +++ b/client/common/ButtonOrLink.tsx @@ -1,14 +1,38 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import PropTypes from 'prop-types'; + /** - * Helper for switching between ); + expect(screen.getByText('Click Me')).toBeInTheDocument(); + }); + + it('calls onClick handler when clicked', () => { + const handleClick = jest.fn(); + render(); + fireEvent.click(screen.getByText('Click')); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it('renders as an anchor when href is provided', () => { + render(); + const anchor = screen.getByRole('link'); + expect(anchor).toHaveAttribute('href', 'https://example.com'); + }); + + it('renders as a React Router link when `to` is provided', () => { + render(); + const link = screen.getByRole('link'); + expect(link).toHaveAttribute('href', '/dashboard'); + }); + + it('renders with iconBefore', () => { + render( + + ); + expect(screen.getByLabelText('github')).toBeInTheDocument(); + }); + + it('renders disabled state', () => { + render(); + expect(screen.getByRole('button')).toBeDisabled(); + }); + + it('uses aria-label when provided', () => { + render(); - expect(screen.getByText('Click Me')).toBeInTheDocument(); - }); - - it('calls onClick handler when clicked', () => { - const handleClick = jest.fn(); - render(); - fireEvent.click(screen.getByText('Click')); - expect(handleClick).toHaveBeenCalledTimes(1); - }); - + // Tag it('renders as an anchor when href is provided', () => { render(); const anchor = screen.getByRole('link'); + expect(anchor.tagName.toLowerCase()).toBe('a'); expect(anchor).toHaveAttribute('href', 'https://example.com'); }); - it('renders as a React Router link when `to` is provided', () => { + it('renders as a React Router when `to` is provided', () => { render(); const link = screen.getByRole('link'); + expect(link.tagName.toLowerCase()).toBe('a'); // Link renders as expect(link).toHaveAttribute('href', '/dashboard'); }); - it('renders with iconBefore', () => { + it('renders as a ); + const el = screen.getByRole('button'); + expect(el.tagName.toLowerCase()).toBe('button'); + }); + + // Children & Icons + it('renders children', () => { + render(); + expect(screen.getByText('Click Me')).toBeInTheDocument(); + }); + + it('renders an iconBefore and button text', () => { + render( + + ); + expect(screen.getByLabelText('iconbefore')).toBeInTheDocument(); + expect(screen.getByRole('button')).toHaveTextContent( + 'This has a before icon' + ); + }); + + it('renders with iconAfter', () => { render( - + + ); + expect(screen.getByLabelText('iconafter')).toBeInTheDocument(); + expect(screen.getByRole('button')).toHaveTextContent( + 'This has an after icon' ); - expect(screen.getByLabelText('github')).toBeInTheDocument(); + }); + + it('renders only the icon if iconOnly', () => { + render( + + ); + expect(screen.getByLabelText('iconafter')).toBeInTheDocument(); + expect(screen.getByRole('button')).not.toHaveTextContent( + 'This has an after icon' + ); + }); + + // HTML attributes + it('calls onClick handler when clicked', () => { + const handleClick = jest.fn(); + render(); + fireEvent.click(screen.getByText('Click')); + expect(handleClick).toHaveBeenCalledTimes(1); }); it('renders disabled state', () => { diff --git a/client/common/Button.tsx b/client/common/Button.tsx index 1b80c6daa2..1d23619e8e 100644 --- a/client/common/Button.tsx +++ b/client/common/Button.tsx @@ -201,7 +201,7 @@ const Button = ({ const content = ( <> {iconBefore} - {hasChildren && {children}} + {hasChildren && !iconOnly && {children}} {iconAfter} ); From 9ff1c04babcf508df9b2dbfa5c1f6df154e3aa67 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sat, 2 Aug 2025 20:27:48 +0100 Subject: [PATCH 16/31] cleanup test --- client/common/Button.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/common/Button.test.tsx b/client/common/Button.test.tsx index d55e23b366..863f2d89b2 100644 --- a/client/common/Button.test.tsx +++ b/client/common/Button.test.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { BrowserRouter } from 'react-router-dom'; import { render, screen, fireEvent } from '../test-utils'; import Button from './Button'; @@ -23,10 +22,11 @@ describe('Button', () => { expect(link).toHaveAttribute('href', '/dashboard'); }); - it('renders as a ); const el = screen.getByRole('button'); expect(el.tagName.toLowerCase()).toBe('button'); + expect(el).toHaveAttribute('type', 'button'); }); // Children & Icons From 10d68d531450b78fb644b3f6854b998cffdacc95 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sat, 2 Aug 2025 20:40:13 +0100 Subject: [PATCH 17/31] usePrevious: migrate to ts --no-verify --- client/common/{usePrevious.js => usePrevious.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/common/{usePrevious.js => usePrevious.ts} (100%) diff --git a/client/common/usePrevious.js b/client/common/usePrevious.ts similarity index 100% rename from client/common/usePrevious.js rename to client/common/usePrevious.ts From fa64e377df35169bd82eeb5c20442b947fca0e1e Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sat, 2 Aug 2025 20:52:51 +0100 Subject: [PATCH 18/31] usePrevious: add types --- client/common/usePrevious.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/client/common/usePrevious.ts b/client/common/usePrevious.ts index ed46581cb0..6f7645e145 100644 --- a/client/common/usePrevious.ts +++ b/client/common/usePrevious.ts @@ -1,7 +1,14 @@ import { useEffect, useRef } from 'react'; -export default function usePrevious(value) { - const ref = useRef(); +/** + * Custom hook to store the previous value of a number. + * + * @param value - The current value to track. + * @returns The previous value before the current render, or undefined if none. + */ +export default function usePrevious(value: number): number | undefined { + // eslint-disable-next-line prettier/prettier + const ref = useRef(); useEffect(() => { ref.current = value; From 55cc1db3cde6ff2a4e506c8cf4218e49ef07ecce Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sat, 2 Aug 2025 21:08:04 +0100 Subject: [PATCH 19/31] fix warning for any --- client/common/Button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/common/Button.tsx b/client/common/Button.tsx index 1d23619e8e..f3cef99612 100644 --- a/client/common/Button.tsx +++ b/client/common/Button.tsx @@ -205,7 +205,7 @@ const Button = ({ {iconAfter} ); - const StyledComponent: React.ElementType = iconOnly ? StyledInlineButton : StyledButton; + const StyledComponent: React.ElementType = iconOnly ? StyledInlineButton : StyledButton; if (href) { return ( From ad6e8ff55bbb3cdc37b5b33b1ed56fc89b7ba1e5 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sat, 2 Aug 2025 21:25:17 +0100 Subject: [PATCH 20/31] useSyncFormTranslations: update to ts --no-verify --- .../{useSyncFormTranslations.js => useSyncFormTranslations.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/common/{useSyncFormTranslations.js => useSyncFormTranslations.ts} (100%) diff --git a/client/common/useSyncFormTranslations.js b/client/common/useSyncFormTranslations.ts similarity index 100% rename from client/common/useSyncFormTranslations.js rename to client/common/useSyncFormTranslations.ts From db9142557706ccc8d314039724172f42e5ecfb40 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sat, 2 Aug 2025 22:48:54 +0100 Subject: [PATCH 21/31] useSyncTranslations: add types and jsdocs --- client/common/useSyncFormTranslations.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/client/common/useSyncFormTranslations.ts b/client/common/useSyncFormTranslations.ts index 411c942363..0a3ed46725 100644 --- a/client/common/useSyncFormTranslations.ts +++ b/client/common/useSyncFormTranslations.ts @@ -1,9 +1,20 @@ -import { useEffect } from 'react'; +import { useEffect, MutableRefObject } from 'react'; -// Usage: useSyncFormTranslations(formRef, language) -// This hook ensures that form values are preserved when the language changes. -// Pass a ref to the form instance and the current language as arguments. -const useSyncFormTranslations = (formRef, language) => { +export interface FormLike { + getState(): { values: Record }; + reset(): void; + change(field: string, value: unknown): void; +} + +/** + * This hook ensures that form values are preserved when the language changes. + * @param formRef + * @param language + */ +const useSyncFormTranslations = ( + formRef: MutableRefObject, + language: string +) => { useEffect(() => { const form = formRef.current; if (!form) return; From 260277baad987f881e23e3694ba53a97837581ee Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sat, 2 Aug 2025 22:51:37 +0100 Subject: [PATCH 22/31] useModalClose: update to ts --no-verify --- client/common/{useModalClose.js => useModalClose.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/common/{useModalClose.js => useModalClose.ts} (100%) diff --git a/client/common/useModalClose.js b/client/common/useModalClose.ts similarity index 100% rename from client/common/useModalClose.js rename to client/common/useModalClose.ts From 1118d08864f8d928e23656ceb4c3eeb42eee303b Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sat, 2 Aug 2025 23:06:14 +0100 Subject: [PATCH 23/31] fix: remove hardcoded babel parser from prettier so prettier can determine which parse to use from eslint rules --- .prettierrc | 1 - client/common/Button.tsx | 117 +++++++++++++++++---------------- client/common/ButtonOrLink.tsx | 26 +++++--- client/common/IconButton.tsx | 4 +- client/common/RouterTab.tsx | 4 +- client/common/usePrevious.ts | 1 - client/components/SkipLink.tsx | 4 +- 7 files changed, 82 insertions(+), 75 deletions(-) diff --git a/.prettierrc b/.prettierrc index e2da8bb25a..df6b0841b0 100644 --- a/.prettierrc +++ b/.prettierrc @@ -5,7 +5,6 @@ "insertPragma": false, "jsxBracketSameLine": false, "jsxSingleQuote": false, - "parser": "babel", "printWidth": 80, "proseWrap": "never", "requirePragma": false, diff --git a/client/common/Button.tsx b/client/common/Button.tsx index f3cef99612..5d1fd619b0 100644 --- a/client/common/Button.tsx +++ b/client/common/Button.tsx @@ -5,88 +5,87 @@ import { remSize, prop } from '../theme'; const kinds = { primary: 'primary', - secondary:'secondary' -// eslint-disable-next-line prettier/prettier + secondary: 'secondary' } as const; -type Kind = keyof typeof kinds +type Kind = keyof typeof kinds; const displays = { block: 'block', inline: 'inline' } as const; -type Display = keyof typeof displays +type Display = keyof typeof displays; type StyledButtonProps = { - kind: Kind, - display: Display -} + kind: Kind; + display: Display; +}; const buttonTypes = { button: 'button', submit: 'submit' } as const; -type ButtonType = keyof typeof buttonTypes +type ButtonType = keyof typeof buttonTypes; type SharedButtonProps = { - /** + /** * The visible part of the button, telling the user what * the action is */ - children?: React.ReactNode, - /** + children?: React.ReactNode; + /** If the button can be activated or not */ - disabled?: boolean, - /** - * The display type of the button—inline or block - */ - display?: Display, - /** - * SVG icon to place after child content - */ - iconAfter?: React.ReactNode, - /** - * SVG icon to place before child content - */ - iconBefore?: React.ReactNode, - /** - * If the button content is only an SVG icon - */ - iconOnly?: boolean, - /** - * The kind of button - determines how it appears visually - */ - kind?: Kind, - /** - * Specifying an href will use an to link to the URL - */ - href?: string | null, - /** - * An ARIA Label used for accessibility - */ - 'aria-label'?: string | null, - /** - * Specifying a to URL will use a react-router Link - */ - to?: string | null, - /** - * If using a button, then type is defines the type of button - */ - type?: ButtonType, - /** - * Allows for IconButton to pass `focusable="false"` as a prop for SVGs. - * See @types/react > interface SVGAttributes extends AriaAttributes, DOMAttributes - */ - focusable?: boolean | 'true' | 'false' -} + disabled?: boolean; + /** + * The display type of the button—inline or block + */ + display?: Display; + /** + * SVG icon to place after child content + */ + iconAfter?: React.ReactNode; + /** + * SVG icon to place before child content + */ + iconBefore?: React.ReactNode; + /** + * If the button content is only an SVG icon + */ + iconOnly?: boolean; + /** + * The kind of button - determines how it appears visually + */ + kind?: Kind; + /** + * Specifying an href will use an to link to the URL + */ + href?: string | null; + /** + * An ARIA Label used for accessibility + */ + 'aria-label'?: string | null; + /** + * Specifying a to URL will use a react-router Link + */ + to?: string | null; + /** + * If using a button, then type is defines the type of button + */ + type?: ButtonType; + /** + * Allows for IconButton to pass `focusable="false"` as a prop for SVGs. + * See @types/react > interface SVGAttributes extends AriaAttributes, DOMAttributes + */ + focusable?: boolean | 'true' | 'false'; +}; export type ButtonProps = SharedButtonProps & -React.ButtonHTMLAttributes & -React.AnchorHTMLAttributes & -Partial; + React.ButtonHTMLAttributes & + React.AnchorHTMLAttributes & + Partial; // The '&&&' will increase the specificity of the // component's CSS so that it overrides the more @@ -205,7 +204,9 @@ const Button = ({ {iconAfter} ); - const StyledComponent: React.ElementType = iconOnly ? StyledInlineButton : StyledButton; + const StyledComponent: React.ElementType = iconOnly + ? StyledInlineButton + : StyledButton; if (href) { return ( diff --git a/client/common/ButtonOrLink.tsx b/client/common/ButtonOrLink.tsx index b403331e3b..b79695048d 100644 --- a/client/common/ButtonOrLink.tsx +++ b/client/common/ButtonOrLink.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { Link } from 'react-router-dom'; - /** * Accepts all the props of an HTML or