From f1af0a6e8c4acf1f8dd1c80cec53cd547db6913f Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 10:41:49 +0100 Subject: [PATCH 01/49] .prettierrc: remove hardcoded babel as parser setting to allow typescript parser on ts files --- .prettierrc | 1 - 1 file changed, 1 deletion(-) 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, From 8a0f302076acdab6bf8786bc84dd993644b45dde Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 10:45:29 +0100 Subject: [PATCH 02/49] SkipLink: lint correctly to add ; to end of type definition lines --- client/components/SkipLink.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/components/SkipLink.tsx b/client/components/SkipLink.tsx index d70af6a999..c5b7b15e57 100644 --- a/client/components/SkipLink.tsx +++ b/client/components/SkipLink.tsx @@ -3,8 +3,8 @@ import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; type SkipLinkProps = { - targetId: string, - text: string + targetId: string; + text: string; }; const SkipLink = ({ targetId, text }: SkipLinkProps) => { From 1cb8f5b0a71c7e3a9028f0eef1752c46d92a958f Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 11:00:39 +0100 Subject: [PATCH 03/49] RouterTab: unit test --- client/common/RouterTab.test.tsx | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) 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..298f91a5f3 --- /dev/null +++ b/client/common/RouterTab.test.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor, history } from '../test-utils'; +import Tab from './RouterTab'; + +const mockPath = '/projects'; +const mockLinkText = 'Projects'; + +describe('Tab', () => { + function rerender() { + return render({mockLinkText}); + } + + it('renders a react-router NavLink with correct text and path', async () => { + rerender(); + + const linkElement = screen.getByText(mockLinkText); + expect(linkElement).toBeInTheDocument(); + expect(linkElement.getAttribute('href')).toBe(mockPath); + + fireEvent.click(linkElement); + await waitFor(() => expect(history.location.pathname).toEqual('/projects')); + }); + + it('includes the dashboard-header class names', () => { + const { container } = rerender(); + + const listItem = container.querySelector('li'); + const link = container.querySelector('a'); + + expect(listItem).toHaveClass('dashboard-header__tab'); + expect(link).toHaveClass('dashboard-header__tab__title'); + }); +}); From d3762079fae4060276678bd87d11038cf218b62f Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 11:02:10 +0100 Subject: [PATCH 04/49] RouterTab: update 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 29e1e6d164800f64146ba8c8120862e6a0b42770 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 11:08:44 +0100 Subject: [PATCH 05/49] RouterTab: add typescript & install @types/react-router-dom --- client/common/RouterTab.tsx | 14 ++++----- package-lock.json | 58 +++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 65 insertions(+), 8 deletions(-) diff --git a/client/common/RouterTab.tsx b/client/common/RouterTab.tsx index d08c839855..cd20455f49 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'; +export 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 1efe93de1b2d9b0fe71f3ab64da459cbc500df0b Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 11:14:11 +0100 Subject: [PATCH 06/49] Button: update to tsx --no-verify --- client/common/{Button.jsx => Button.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/common/{Button.jsx => Button.tsx} (100%) diff --git a/client/common/Button.jsx b/client/common/Button.tsx similarity index 100% rename from client/common/Button.jsx rename to client/common/Button.tsx From 60426387cd1669399592cb9f029866474f099147 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 11:21:57 +0100 Subject: [PATCH 07/49] Button.tsx: migrate to typescript, add unit test, add @types/styled-components, remove react/require-default-props rule --- .eslintrc | 3 +- client/common/Button.test.tsx | 91 ++++++++++++++++++ client/common/Button.tsx | 168 ++++++++++++++++++---------------- package-lock.json | 39 ++++++++ package.json | 1 + 5 files changed, 220 insertions(+), 82 deletions(-) create mode 100644 client/common/Button.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/Button.test.tsx b/client/common/Button.test.tsx new file mode 100644 index 0000000000..863f2d89b2 --- /dev/null +++ b/client/common/Button.test.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { render, screen, fireEvent } from '../test-utils'; +import Button from './Button'; + +const MockIcon = (props: React.SVGProps) => ( + +); + +describe('Button', () => { + // Tag + it('renders as an anchor when href is provided', () => { + render(Link); + const anchor = screen.getByRole('link'); + expect(anchor.tagName.toLowerCase()).toBe('a'); + expect(anchor).toHaveAttribute('href', 'https://example.com'); + }); + + it('renders as a React Router when `to` is provided', () => { + render(Go); + const link = screen.getByRole('link'); + expect(link.tagName.toLowerCase()).toBe('a'); // Link renders as + expect(link).toHaveAttribute('href', '/dashboard'); + }); + + it('renders as a with a type of "button" by default', () => { + render(Click Me); + const el = screen.getByRole('button'); + expect(el.tagName.toLowerCase()).toBe('button'); + expect(el).toHaveAttribute('type', 'button'); + }); + + // Children & Icons + it('renders children', () => { + render(Click Me); + expect(screen.getByText('Click Me')).toBeInTheDocument(); + }); + + it('renders an iconBefore and button text', () => { + render( + }> + This has a before icon + + ); + expect(screen.getByLabelText('iconbefore')).toBeInTheDocument(); + expect(screen.getByRole('button')).toHaveTextContent( + 'This has a before icon' + ); + }); + + it('renders with iconAfter', () => { + render( + }> + This has an after icon + + ); + expect(screen.getByLabelText('iconafter')).toBeInTheDocument(); + expect(screen.getByRole('button')).toHaveTextContent( + 'This has an after icon' + ); + }); + + it('renders only the icon if iconOnly', () => { + render( + } iconOnly> + This has an after icon + + ); + 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(Click); + fireEvent.click(screen.getByText('Click')); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it('renders disabled state', () => { + render(Disabled); + expect(screen.getByRole('button')).toBeDisabled(); + }); + + it('uses aria-label when provided', () => { + render(); + expect(screen.getByLabelText('Upload')).toBeInTheDocument(); + }); +}); diff --git a/client/common/Button.tsx b/client/common/Button.tsx index 516536fb2b..cc83ad26d0 100644 --- a/client/common/Button.tsx +++ b/client/common/Button.tsx @@ -1,24 +1,94 @@ import React from 'react'; -import PropTypes from 'prop-types'; import styled from 'styled-components'; -import { Link } from 'react-router-dom'; - +import { Link, LinkProps } from 'react-router-dom'; import { remSize, prop } from '../theme'; const kinds = { primary: 'primary', secondary: 'secondary' -}; +} as const; const displays = { block: 'block', inline: 'inline' +} as const; + +const buttonTypes = { + button: 'button', + submit: 'submit' +} as const; + +type Kind = keyof typeof kinds; +type Display = keyof typeof displays; +type ButtonType = keyof typeof buttonTypes; + +type StyledButtonProps = { + kind: Kind; + display: Display; +}; + +type SharedButtonProps = { + /** + * The visible part of the button, telling the user what + * the action is + */ + 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'; }; +export type ButtonProps = SharedButtonProps & + React.ButtonHTMLAttributes & + React.AnchorHTMLAttributes & + Partial; + // The '&&&' will increase the specificity of the // component's CSS so that it overrides the more // general global styles -const StyledButton = styled.button` +const StyledButton = styled.button` &&& { font-weight: bold; display: ${({ display }) => @@ -112,31 +182,29 @@ const StyledInlineButton = styled.button` * A Button performs an primary action */ const Button = ({ - children, - display, + children = null, + display = displays.block, href, - kind, - iconBefore, - iconAfter, - iconOnly, + kind = kinds.primary, + iconBefore = null, + iconAfter = null, + iconOnly = false, 'aria-label': ariaLabel, to, - type, + type = buttonTypes.button, ...props -}) => { +}: ButtonProps) => { const hasChildren = React.Children.count(children) > 0; const content = ( <> {iconBefore} - {hasChildren && {children}} + {hasChildren && !iconOnly && {children}} {iconAfter} > ); - let StyledComponent = StyledButton; - - if (iconOnly) { - StyledComponent = StyledInlineButton; - } + const StyledComponent: React.ElementType = iconOnly + ? StyledInlineButton + : StyledButton; if (href) { return ( @@ -181,69 +249,7 @@ const Button = ({ ); }; -Button.defaultProps = { - children: null, - disabled: false, - display: displays.block, - iconAfter: null, - iconBefore: null, - iconOnly: false, - kind: kinds.primary, - href: null, - 'aria-label': null, - to: null, - type: 'button' -}; - Button.kinds = kinds; Button.displays = displays; -Button.propTypes = { - /** - * The visible part of the button, telling the user what - * the action is - */ - children: PropTypes.oneOfType([PropTypes.element, PropTypes.string]), - /** - If the button can be activated or not - */ - disabled: PropTypes.bool, - /** - * The display type of the button—inline or block - */ - display: PropTypes.oneOf(Object.values(displays)), - /** - * SVG icon to place after child content - */ - iconAfter: PropTypes.element, - /** - * SVG icon to place before child content - */ - iconBefore: PropTypes.element, - /** - * If the button content is only an SVG icon - */ - iconOnly: PropTypes.bool, - /** - * The kind of button - determines how it appears visually - */ - kind: PropTypes.oneOf(Object.values(kinds)), - /** - * Specifying an href will use an to link to the URL - */ - href: PropTypes.string, - /** - * An ARIA Label used for accessibility - */ - 'aria-label': PropTypes.string, - /** - * Specifying a to URL will use a react-router Link - */ - to: PropTypes.string, - /** - * If using a button, then type is defines the type of button - */ - type: PropTypes.oneOf(['button', 'submit']) -}; - export default Button; 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 c1883c534827b2a4d88c5e3598f375ce8fe507c5 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 11:30:36 +0100 Subject: [PATCH 08/49] ButtonOrLink: update to tsx files --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 eb691385c08a1db46c089d0b66f1f044488389cd Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 11:41:11 +0100 Subject: [PATCH 09/49] ButtonOrLink: update to typescript --- client/common/ButtonOrLink.tsx | 73 +++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/client/common/ButtonOrLink.tsx b/client/common/ButtonOrLink.tsx index f759aa8ffb..558a1c41a3 100644 --- a/client/common/ButtonOrLink.tsx +++ b/client/common/ButtonOrLink.tsx @@ -1,14 +1,47 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import PropTypes from 'prop-types'; /** - * Helper for switching between , , and + * Accepts all the props of an HTML or tag. */ +export type ButtonOrLinkProps = { + /** + * Can be internal or external ('http'- or 'https'-). + */ + href?: string; + isDisabled?: boolean; + /** + * Content of the button/link. + * Can be either a string or a complex element. + */ + children: React.ReactNode; + onClick?: ( + e: React.MouseEvent + ) => void; +}; + +export type Ref = HTMLAnchorElement | HTMLButtonElement; +/** + * Helper for switching between ``, ``, and `` + * If providing an `href`, will render as a link instead of a button. + * - Internal links will use react-router. + * - External links should start with 'http' or 'https' and will open in a new window. + */ const ButtonOrLink = React.forwardRef( - ({ href, children, isDisabled, onClick, ...props }, ref) => { - const handleClick = (e) => { + ( + { + href, + children, + isDisabled = false, + onClick, + ...props + }: ButtonOrLinkProps, + ref: React.Ref + ) => { + const handleClick = ( + e: React.MouseEvent + ) => { if (isDisabled) { e.preventDefault(); e.stopPropagation(); @@ -23,7 +56,7 @@ const ButtonOrLink = React.forwardRef( if (href.startsWith('http')) { return ( } href={href} target="_blank" rel="noopener noreferrer" @@ -37,7 +70,7 @@ const ButtonOrLink = React.forwardRef( } return ( } to={href} aria-disabled={isDisabled} {...props} @@ -49,7 +82,7 @@ const ButtonOrLink = React.forwardRef( } return ( } aria-disabled={isDisabled} {...props} onClick={handleClick} @@ -60,30 +93,4 @@ const ButtonOrLink = React.forwardRef( } ); -/** - * Accepts all the props of an HTML or tag. - */ -ButtonOrLink.propTypes = { - /** - * If providing an href, will render as a link instead of a button. - * Can be internal or external. - * Internal links will use react-router. - * External links should start with 'http' or 'https' and will open in a new window. - */ - href: PropTypes.string, - isDisabled: PropTypes.bool, - /** - * Content of the button/link. - * Can be either a string or a complex element. - */ - children: PropTypes.node.isRequired, - onClick: PropTypes.func -}; - -ButtonOrLink.defaultProps = { - href: null, - isDisabled: false, - onClick: null -}; - export default ButtonOrLink; From 5dd03d1ac1c50272f9ae9176a0c47953fefe3778 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 12:24:15 +0100 Subject: [PATCH 10/49] 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 197fd67e1547082533b429be4d9f5264fb9e369a Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 12:38:45 +0100 Subject: [PATCH 11/49] IconButton: migrate to typescript & add unit test --- client/common/IconButton.test.tsx | 27 +++++++++++++++++++++ client/common/IconButton.tsx | 39 +++++++++++++------------------ 2 files changed, 43 insertions(+), 23 deletions(-) create mode 100644 client/common/IconButton.test.tsx 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 8cf732f91d..9739bb3fcd 100644 --- a/client/common/IconButton.tsx +++ b/client/common/IconButton.tsx @@ -1,7 +1,6 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { ComponentType } from 'react'; import styled from 'styled-components'; -import Button from './Button'; +import Button, { ButtonProps } from './Button'; import { remSize } from '../theme'; const ButtonWrapper = styled(Button)` @@ -12,27 +11,21 @@ const ButtonWrapper = styled(Button)` } `; -const IconButton = (props) => { - const { icon, ...otherProps } = props; - const Icon = icon; - - return ( - } - iconOnly - display={Button.displays.inline} - focusable="false" - {...otherProps} - /> - ); -}; - -IconButton.propTypes = { - icon: PropTypes.func +export type IconButtonProps = Omit< + ButtonProps, + 'iconBefore' | 'display' | 'focusable' +> & { + icon?: ComponentType<{ 'aria-label'?: string }> | null; }; -IconButton.defaultProps = { - icon: null -}; +const IconButton = ({ icon: Icon, ...otherProps }: IconButtonProps) => ( + : undefined} + iconOnly + display={Button.displays.inline} + focusable="false" + {...otherProps} + /> +); export default IconButton; From cfb4b827ae3e45268ef10ce36e69936e498d61fe Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 12:47:11 +0100 Subject: [PATCH 12/49] usePrevious: update 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 d80961ca84f2efa29df07f52b285a295813a795e Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 12:51:53 +0100 Subject: [PATCH 13/49] usePrevious: migrate to typescript & add unit test --- client/common/usePrevious.test.tsx | 37 ++++++++++++++++++++++++++++++ client/common/usePrevious.ts | 9 ++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 client/common/usePrevious.test.tsx diff --git a/client/common/usePrevious.test.tsx b/client/common/usePrevious.test.tsx new file mode 100644 index 0000000000..13a1731feb --- /dev/null +++ b/client/common/usePrevious.test.tsx @@ -0,0 +1,37 @@ +import React, { useEffect, useState } from 'react'; +import { render, screen } from '../test-utils'; +import usePrevious from './usePrevious'; + +function TestComponent({ value }: { value: number }) { + const prev = usePrevious(value); + + return ( + + current: {value} + previous: {prev ?? 'undefined'} + + ); +} + +describe('usePrevious', () => { + it('should return undefined on first render and previous value after update', () => { + const { rerender } = render(); + + // First render: previous should be undefined + expect(screen.getByText('current: 1')).toBeInTheDocument(); + expect(screen.getByText('previous: undefined')).toBeInTheDocument(); + + // Update value + rerender(); + + // Second render: previous should be 1 + expect(screen.getByText('current: 2')).toBeInTheDocument(); + expect(screen.getByText('previous: 1')).toBeInTheDocument(); + + // Update value again + rerender(); + + expect(screen.getByText('current: 3')).toBeInTheDocument(); + expect(screen.getByText('previous: 2')).toBeInTheDocument(); + }); +}); diff --git a/client/common/usePrevious.ts b/client/common/usePrevious.ts index ed46581cb0..b285241130 100644 --- a/client/common/usePrevious.ts +++ b/client/common/usePrevious.ts @@ -1,7 +1,12 @@ import { useEffect, useRef } from 'react'; -export default function usePrevious(value) { - const ref = useRef(); +/** + * Used in Menubar 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 { + const ref = useRef(); useEffect(() => { ref.current = value; From 24368ccdb46708545a077c64f5f90abe31a4bc14 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 12:58:49 +0100 Subject: [PATCH 14/49] isMac: migrate to typescript and add test --- .eslintrc | 1 + client/utils/device.js | 1 - client/utils/device.test.ts | 45 +++++++++++++++++++++++++++++++++++++ client/utils/device.ts | 10 +++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) delete mode 100644 client/utils/device.js create mode 100644 client/utils/device.test.ts create mode 100644 client/utils/device.ts diff --git a/.eslintrc b/.eslintrc index 0b800e2da2..c02e312763 100644 --- a/.eslintrc +++ b/.eslintrc @@ -30,6 +30,7 @@ } ], "react/jsx-filename-extension": [1, { "extensions": [".jsx", ".tsx"] }], + "import/prefer-default-export": "off", "comma-dangle": 0, // not sure why airbnb turned this on. gross! "default-param-last": 0, "no-else-return" :0, diff --git a/client/utils/device.js b/client/utils/device.js deleted file mode 100644 index 040b16b7d4..0000000000 --- a/client/utils/device.js +++ /dev/null @@ -1 +0,0 @@ -export const isMac = () => navigator.userAgent.toLowerCase().indexOf('mac') !== -1; // eslint-disable-line diff --git a/client/utils/device.test.ts b/client/utils/device.test.ts new file mode 100644 index 0000000000..b748bb47f1 --- /dev/null +++ b/client/utils/device.test.ts @@ -0,0 +1,45 @@ +import { isMac } from './device'; + +describe('isMac', () => { + const originalUserAgent = navigator.userAgent; + + afterEach(() => { + // Restore the original userAgent after each test + Object.defineProperty(navigator, 'userAgent', { + value: originalUserAgent, + configurable: true + }); + }); + + it('returns true when userAgent contains "Mac"', () => { + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', + configurable: true + }); + expect(isMac()).toBe(true); + }); + + it('returns false when userAgent does not contain "Mac"', () => { + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', + configurable: true + }); + expect(isMac()).toBe(false); + }); + + it('returns false when navigator agent is null', () => { + Object.defineProperty(navigator, 'userAgent', { + value: null, + configurable: true + }); + expect(isMac()).toBe(false); + }); + + it('returns false when navigator agent is undefined', () => { + Object.defineProperty(navigator, 'userAgent', { + value: undefined, + configurable: true + }); + expect(isMac()).toBe(false); + }); +}); diff --git a/client/utils/device.ts b/client/utils/device.ts new file mode 100644 index 0000000000..db9ffe1980 --- /dev/null +++ b/client/utils/device.ts @@ -0,0 +1,10 @@ +/** + * Checks if the user's OS is macOS based on the user agent string. + * This is the preferred method over navigator.platform, which is now deprecated: + * - see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform + */ +export function isMac(): boolean { + return typeof navigator?.userAgent === 'string' + ? navigator.userAgent.toLowerCase().includes('mac') + : false; +} From e67e015c0d737e4e754007d44ba9951bdbffeb59 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 13:02:04 +0100 Subject: [PATCH 15/49] useKeyDownHandlers: update to ts --no-verify --- client/common/{useKeyDownHandlers.js => useKeyDownHandlers.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/common/{useKeyDownHandlers.js => useKeyDownHandlers.ts} (100%) diff --git a/client/common/useKeyDownHandlers.js b/client/common/useKeyDownHandlers.ts similarity index 100% rename from client/common/useKeyDownHandlers.js rename to client/common/useKeyDownHandlers.ts From 1b5373c4e8e0fc8644c7b6f46d17727c7e8282c1 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 13:11:54 +0100 Subject: [PATCH 16/49] useKeyDownHandler: migrate to typescript and add unit test --- .eslintrc | 2 +- client/common/useKeyDownHandlers.test.tsx | 82 +++++++++++++++++++++++ client/common/useKeyDownHandlers.ts | 37 +++------- 3 files changed, 93 insertions(+), 28 deletions(-) create mode 100644 client/common/useKeyDownHandlers.test.tsx diff --git a/.eslintrc b/.eslintrc index c02e312763..8de3693a9e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -29,8 +29,8 @@ "tsx": "never" } ], - "react/jsx-filename-extension": [1, { "extensions": [".jsx", ".tsx"] }], "import/prefer-default-export": "off", + "react/jsx-filename-extension": [1, { "extensions": [".jsx", ".tsx"] }], "comma-dangle": 0, // not sure why airbnb turned this on. gross! "default-param-last": 0, "no-else-return" :0, diff --git a/client/common/useKeyDownHandlers.test.tsx b/client/common/useKeyDownHandlers.test.tsx new file mode 100644 index 0000000000..f1d3480d96 --- /dev/null +++ b/client/common/useKeyDownHandlers.test.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import useKeyDownHandlers from './useKeyDownHandlers'; +import { isMac } from '../utils/device'; + +jest.mock('../utils/device'); + +function fireKeyboardEvent(key: string, options: Partial = {}) { + const event = new KeyboardEvent('keydown', { + key, + code: `Key${key.toUpperCase()}`, + bubbles: true, + cancelable: true, + ...options + }); + document.dispatchEvent(event); +} + +// Component for testing the hook +const HookConsumer = ({ + handlers +}: { + handlers: Record; +}) => { + useKeyDownHandlers(handlers); + return null; +}; + +describe('useKeyDownHandlers', () => { + let handlers: Record; + + beforeEach(() => { + handlers = { + f: jest.fn(), + 'ctrl-f': jest.fn(), + 'ctrl-shift-f': jest.fn(), + 'ctrl-alt-n': jest.fn() + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('calls "ctrl-f" handler on Windows (isMac false)', () => { + (isMac as jest.Mock).mockReturnValue(false); + + render(); + fireKeyboardEvent('f', { ctrlKey: true }); + + expect(handlers['ctrl-f']).toHaveBeenCalled(); + expect(handlers.f).toHaveBeenCalled(); + }); + + it('calls "ctrl-f" handler on Mac (isMac true)', () => { + (isMac as jest.Mock).mockReturnValue(true); + + render(); + fireKeyboardEvent('f', { metaKey: true }); + + expect(handlers['ctrl-f']).toHaveBeenCalled(); + expect(handlers.f).toHaveBeenCalled(); + }); + + it('calls "ctrl-shift-f" handler with both ctrl and shift keys', () => { + (isMac as jest.Mock).mockReturnValue(false); + + render(); + fireKeyboardEvent('f', { ctrlKey: true, shiftKey: true }); + + expect(handlers['ctrl-shift-f']).toHaveBeenCalled(); + }); + + it('calls "ctrl-alt-n" handler with ctrl and alt', () => { + (isMac as jest.Mock).mockReturnValue(false); + + render(); + fireKeyboardEvent('n', { ctrlKey: true, altKey: true, code: 'KeyN' }); + + expect(handlers['ctrl-alt-n']).toHaveBeenCalled(); + }); +}); diff --git a/client/common/useKeyDownHandlers.ts b/client/common/useKeyDownHandlers.ts index 7259574e82..2c8da5a3eb 100644 --- a/client/common/useKeyDownHandlers.ts +++ b/client/common/useKeyDownHandlers.ts @@ -1,18 +1,19 @@ import { mapKeys } from 'lodash'; -import PropTypes from 'prop-types'; import { useCallback, useEffect, useRef } from 'react'; +import { isMac } from '../utils/device'; + +/** Function to call upon keydown */ +export type KeydownHandler = (e: KeyboardEvent) => void; +/** An object mapping from keys like 'ctrl-s' or 'ctrl-shift-1' to handlers. */ +export type KeydownHandlerMap = Record; /** * Attaches keydown handlers to the global document. - * * Handles Mac/PC switching of Ctrl to Cmd. - * - * @param {Record void>} keyHandlers - an object - * which maps from the key to its event handler. The object keys are a combination - * of the key and prefixes `ctrl-` `shift-` (ie. 'ctrl-f', 'ctrl-shift-f') - * and the values are the function to call when that specific key is pressed. + * @param keyHandlers - an object which maps from the key to its event handler. The object keys are a combination of the key and prefixes `ctrl-` `shift-` + * (ie. 'ctrl-f', 'ctrl-shift-f') and the values are the function to call when that specific key is pressed. */ -export default function useKeyDownHandlers(keyHandlers) { +export default function useKeyDownHandlers(keyHandlers: KeydownHandlerMap) { /** * Instead of memoizing the handlers, use a ref and call the current * handler at the time of the event. @@ -30,8 +31,7 @@ export default function useKeyDownHandlers(keyHandlers) { */ const handleEvent = useCallback((e) => { if (!e.key) return; - const isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1; - const isCtrl = isMac ? e.metaKey : e.ctrlKey; + const isCtrl = isMac() ? e.metaKey : e.ctrlKey; if (e.shiftKey && isCtrl) { handlers.current[ `ctrl-shift-${ @@ -53,20 +53,3 @@ export default function useKeyDownHandlers(keyHandlers) { return () => document.removeEventListener('keydown', handleEvent); }, [handleEvent]); } - -/** - * Component version can be used in class components where hooks can't be used. - * - * @param {Record void>} handlers - */ -export const DocumentKeyDown = ({ handlers }) => { - useKeyDownHandlers(handlers); - return null; -}; -DocumentKeyDown.propTypes = { - handlers: PropTypes.objectOf(PropTypes.func) -}; - -DocumentKeyDown.defaultProps = { - handlers: {} -}; From b5eea95fac73f15b493e3a29f9dfd1b5de0685e5 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 13:17:54 +0100 Subject: [PATCH 17/49] 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 7ca1e6d441a5eeb76dae6e724322c49d17e1f372 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 13:27:41 +0100 Subject: [PATCH 18/49] useModalClose: update to typescript & add test --- client/common/useModalClose.test.tsx | 65 ++++++++++++++++++++++++++++ client/common/useModalClose.ts | 23 +++++----- 2 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 client/common/useModalClose.test.tsx diff --git a/client/common/useModalClose.test.tsx b/client/common/useModalClose.test.tsx new file mode 100644 index 0000000000..9019794d64 --- /dev/null +++ b/client/common/useModalClose.test.tsx @@ -0,0 +1,65 @@ +import React, { useRef, useEffect } from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import useModalClose from './useModalClose'; +import useKeyDownHandlers from './useKeyDownHandlers'; + +jest.mock('./useKeyDownHandlers'); + +describe('useModalClose', () => { + let onClose: jest.Mock; + + beforeEach(() => { + onClose = jest.fn(); + jest.clearAllMocks(); + }); + + function TestModal({ handleClose }: { handleClose: () => void }) { + const ref = useModalClose(handleClose); + return ( + + Outside + } + tabIndex={-1} + style={{ border: '1px solid black' }} + > + Modal content + + + ); + } + + function rerender() { + return render(); + } + + it('calls onClose when clicking outside the modal', () => { + const { getByTestId } = rerender(); + + fireEvent.click(getByTestId('outside')); + + expect(onClose).toHaveBeenCalled(); + }); + + it('does not call onClose when clicking inside the modal', () => { + const { getByTestId } = rerender(); + + fireEvent.click(getByTestId('modal')); + + expect(onClose).not.toHaveBeenCalled(); + }); + + it('returns a ref that is focused on mount', () => { + const { getByTestId } = rerender(); + const modal = getByTestId('modal'); + + expect(document.activeElement).toBe(modal); + }); + + it('calls useKeyDownHandlers with escape handler', () => { + rerender(); + + expect(useKeyDownHandlers).toHaveBeenCalledWith({ escape: onClose }); + }); +}); diff --git a/client/common/useModalClose.ts b/client/common/useModalClose.ts index 2bab24b5de..cd19da2b04 100644 --- a/client/common/useModalClose.ts +++ b/client/common/useModalClose.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, MutableRefObject } from 'react'; import useKeyDownHandlers from './useKeyDownHandlers'; /** @@ -14,21 +14,24 @@ import useKeyDownHandlers from './useKeyDownHandlers'; * * Returns a ref to attach to the outermost element of the modal. * - * @param {() => void} onClose - * @param {React.MutableRefObject} [passedRef] - * @return {React.MutableRefObject} + * @param onClose - Function called when modal should close + * @param passedRef - Optional ref to the modal element. If not provided, one is created internally. + * @returns A ref to be attached to the modal DOM element */ -export default function useModalClose(onClose, passedRef) { - const createdRef = useRef(null); - const modalRef = passedRef || createdRef; +export default function useModalClose( + onClose: () => void, + passedRef?: MutableRefObject +): MutableRefObject { + const createdRef = useRef(null); + const modalRef = passedRef ?? createdRef; useEffect(() => { modalRef.current?.focus(); - function handleClick(e) { + function handleClick(e: MouseEvent) { // ignore clicks on the component itself - if (modalRef.current && !modalRef.current.contains(e.target)) { - onClose?.(); + if (modalRef.current && !modalRef.current.contains(e.target as Node)) { + onClose(); } } From 7075ffbdebcab984308b599e0d84f42279bc8130 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 13:36:02 +0100 Subject: [PATCH 19/49] useSyncFormTranslation: 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 6bc93698b1412b704fe5b36a8746c2ba92d5e4fe Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 13:41:59 +0100 Subject: [PATCH 20/49] useSyncFormTranslations: update to typescript and add unit test --- .../common/useSyncFormTranslations.test.tsx | 46 +++++++++++++++++++ client/common/useSyncFormTranslations.ts | 21 +++++++-- 2 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 client/common/useSyncFormTranslations.test.tsx diff --git a/client/common/useSyncFormTranslations.test.tsx b/client/common/useSyncFormTranslations.test.tsx new file mode 100644 index 0000000000..63622de2e0 --- /dev/null +++ b/client/common/useSyncFormTranslations.test.tsx @@ -0,0 +1,46 @@ +import React, { useRef, useState, useEffect } from 'react'; +import { render } from '@testing-library/react'; +import useSyncFormTranslations from './useSyncFormTranslations'; + +describe('useSyncFormTranslations', () => { + it('resets and re-applies non-empty form values on language change', () => { + const reset = jest.fn(); + const change = jest.fn(); + const getState = jest.fn().mockReturnValue({ + values: { + name: 'Alice', + email: 'alice@example.com', + emptyField: '' + } + }); + + const formMock = { getState, reset, change }; + + function TestComponent({ language }: { language: string }) { + const formRef = useRef(formMock); + useSyncFormTranslations(formRef, language); + + return null; + } + + const { rerender } = render(); + rerender(); // simulate language change + + expect(reset).toHaveBeenCalled(); + + expect(change).toHaveBeenCalledWith('name', 'Alice'); + expect(change).toHaveBeenCalledWith('email', 'alice@example.com'); + expect(change).not.toHaveBeenCalledWith('emptyField', ''); + }); + + it('does nothing if formRef.current is null', () => { + function TestComponent({ language }: { language: string }) { + const formRef = useRef(null); + useSyncFormTranslations(formRef, language); + return null; + } + + render(); + // No error = pass + }); +}); 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 94ea59a4f757419848fc94ad3b7914d38b302f28 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 13:43:05 +0100 Subject: [PATCH 21/49] rename test utility rerender to mountComponent --- client/common/RouterTab.test.tsx | 6 +++--- client/common/useModalClose.test.tsx | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/common/RouterTab.test.tsx b/client/common/RouterTab.test.tsx index 298f91a5f3..95f451ffef 100644 --- a/client/common/RouterTab.test.tsx +++ b/client/common/RouterTab.test.tsx @@ -6,12 +6,12 @@ const mockPath = '/projects'; const mockLinkText = 'Projects'; describe('Tab', () => { - function rerender() { + function mountComponent() { return render({mockLinkText}); } it('renders a react-router NavLink with correct text and path', async () => { - rerender(); + mountComponent(); const linkElement = screen.getByText(mockLinkText); expect(linkElement).toBeInTheDocument(); @@ -22,7 +22,7 @@ describe('Tab', () => { }); it('includes the dashboard-header class names', () => { - const { container } = rerender(); + const { container } = mountComponent(); const listItem = container.querySelector('li'); const link = container.querySelector('a'); diff --git a/client/common/useModalClose.test.tsx b/client/common/useModalClose.test.tsx index 9019794d64..64d26f7308 100644 --- a/client/common/useModalClose.test.tsx +++ b/client/common/useModalClose.test.tsx @@ -30,12 +30,12 @@ describe('useModalClose', () => { ); } - function rerender() { + function mountComponent() { return render(); } it('calls onClose when clicking outside the modal', () => { - const { getByTestId } = rerender(); + const { getByTestId } = mountComponent(); fireEvent.click(getByTestId('outside')); @@ -43,7 +43,7 @@ describe('useModalClose', () => { }); it('does not call onClose when clicking inside the modal', () => { - const { getByTestId } = rerender(); + const { getByTestId } = mountComponent(); fireEvent.click(getByTestId('modal')); @@ -51,14 +51,14 @@ describe('useModalClose', () => { }); it('returns a ref that is focused on mount', () => { - const { getByTestId } = rerender(); + const { getByTestId } = mountComponent(); const modal = getByTestId('modal'); expect(document.activeElement).toBe(modal); }); it('calls useKeyDownHandlers with escape handler', () => { - rerender(); + mountComponent(); expect(useKeyDownHandlers).toHaveBeenCalledWith({ escape: onClose }); }); From 7ce4f54de42078f48d215dd7698d287e51fdbed1 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Fri, 15 Aug 2025 23:35:39 +0100 Subject: [PATCH 22/49] .eslintrc: update to fix no-shadow eslint error on enums --- .eslintrc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index 8de3693a9e..7371a6155a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -133,7 +133,9 @@ "no-use-before-define": "off", "import/no-extraneous-dependencies": "off", "no-unused-vars": "off", - "react/require-default-props": "off" + "react/require-default-props": "off", + "no-shadow": "off", + "@typescript-eslint/no-shadow": "error" } }, { From 7188c1bc9c0ff21b14a997508138197581481c40 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Fri, 15 Aug 2025 23:36:23 +0100 Subject: [PATCH 23/49] usePrevious: make type unknown to make more generic --- client/common/usePrevious.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/common/usePrevious.ts b/client/common/usePrevious.ts index b285241130..2919694db5 100644 --- a/client/common/usePrevious.ts +++ b/client/common/usePrevious.ts @@ -1,12 +1,12 @@ import { useEffect, useRef } from 'react'; /** - * Used in Menubar to store the previous value of a number. + * Used in Menubar to store the previous value. * @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 { - const ref = useRef(); +export default function usePrevious(value: unknown): unknown | undefined { + const ref = useRef(); useEffect(() => { ref.current = value; From bee321192849ffeaba3c9cea68995b7a8d180670 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Fri, 15 Aug 2025 23:37:04 +0100 Subject: [PATCH 24/49] RouterTab: update TabProps to interface --- client/common/RouterTab.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/common/RouterTab.tsx b/client/common/RouterTab.tsx index cd20455f49..571cf5ddd2 100644 --- a/client/common/RouterTab.tsx +++ b/client/common/RouterTab.tsx @@ -1,10 +1,11 @@ import React, { ReactNode } from 'react'; import { NavLink } from 'react-router-dom'; -export type TabProps = { +export interface TabProps { children: ReactNode; to: string; -}; +} + /** * Wraps the react-router `NavLink` with dashboard-header__tab styling. */ From 6148c331929055a2c8e5efdf1013c2889e321800 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sat, 16 Aug 2025 10:51:39 +0100 Subject: [PATCH 25/49] ButtonOrLink: update to interface --- client/common/ButtonOrLink.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/common/ButtonOrLink.tsx b/client/common/ButtonOrLink.tsx index 558a1c41a3..ac5bc7ff4a 100644 --- a/client/common/ButtonOrLink.tsx +++ b/client/common/ButtonOrLink.tsx @@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'; /** * Accepts all the props of an HTML or tag. */ -export type ButtonOrLinkProps = { +export interface ButtonOrLinkProps { /** * Can be internal or external ('http'- or 'https'-). */ @@ -18,7 +18,7 @@ export type ButtonOrLinkProps = { onClick?: ( e: React.MouseEvent ) => void; -}; +} export type Ref = HTMLAnchorElement | HTMLButtonElement; From 3e8faeaceac58332815c5301194415ec57697e5f Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sat, 16 Aug 2025 10:52:14 +0100 Subject: [PATCH 26/49] IconButton: remove null --- client/common/IconButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/common/IconButton.tsx b/client/common/IconButton.tsx index 9739bb3fcd..977a362dc4 100644 --- a/client/common/IconButton.tsx +++ b/client/common/IconButton.tsx @@ -15,7 +15,7 @@ export type IconButtonProps = Omit< ButtonProps, 'iconBefore' | 'display' | 'focusable' > & { - icon?: ComponentType<{ 'aria-label'?: string }> | null; + icon?: ComponentType<{ 'aria-label'?: string }>; }; const IconButton = ({ icon: Icon, ...otherProps }: IconButtonProps) => ( From 4bd9443adbcd4952ff6de9e984983c0e948310ae Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 18 Aug 2025 17:23:31 +0100 Subject: [PATCH 27/49] Button: WIP update --- client/common/Button.tsx | 63 +++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/client/common/Button.tsx b/client/common/Button.tsx index cc83ad26d0..b7825846c9 100644 --- a/client/common/Button.tsx +++ b/client/common/Button.tsx @@ -3,31 +3,28 @@ import styled from 'styled-components'; import { Link, LinkProps } from 'react-router-dom'; import { remSize, prop } from '../theme'; -const kinds = { - primary: 'primary', - secondary: 'secondary' -} as const; +enum Kinds { + primary = 'primary', + secondary = 'secondary' +} -const displays = { - block: 'block', - inline: 'inline' -} as const; +enum Displays { + block = 'block', + inline = 'inline' +} -const buttonTypes = { - button: 'button', - submit: 'submit' -} as const; - -type Kind = keyof typeof kinds; -type Display = keyof typeof displays; -type ButtonType = keyof typeof buttonTypes; +enum ButtonTypes { + button = 'button', + submit = 'submit' +} type StyledButtonProps = { - kind: Kind; - display: Display; -}; + kind: ButtonTypes; + display: Displays; + type?: ButtonTypes; +} & React.ButtonHTMLAttributes; -type SharedButtonProps = { +interface SharedButtonProps { /** * The visible part of the button, telling the user what * the action is @@ -40,7 +37,7 @@ type SharedButtonProps = { /** * The display type of the button—inline or block */ - display?: Display; + display?: Displays; /** * SVG icon to place after child content */ @@ -56,29 +53,29 @@ type SharedButtonProps = { /** * The kind of button - determines how it appears visually */ - kind?: Kind; + kind?: Kinds; /** * Specifying an href will use an to link to the URL */ - href?: string | null; + href?: string; /** * An ARIA Label used for accessibility */ - 'aria-label'?: string | null; + 'aria-label'?: string; /** * Specifying a to URL will use a react-router Link */ - to?: string | null; + to?: string; /** * If using a button, then type is defines the type of button */ - type?: ButtonType; + type?: ButtonTypes; /** * 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 & @@ -92,7 +89,7 @@ const StyledButton = styled.button` &&& { font-weight: bold; display: ${({ display }) => - display === displays.inline ? 'inline-flex' : 'flex'}; + display === Displays.inline ? 'inline-flex' : 'flex'}; justify-content: center; align-items: center; @@ -183,15 +180,15 @@ const StyledInlineButton = styled.button` */ const Button = ({ children = null, - display = displays.block, + display = Displays.block, href, - kind = kinds.primary, + kind = Kinds.primary, iconBefore = null, iconAfter = null, iconOnly = false, 'aria-label': ariaLabel, to, - type = buttonTypes.button, + type = ButtonTypes.button, ...props }: ButtonProps) => { const hasChildren = React.Children.count(children) > 0; @@ -249,7 +246,7 @@ const Button = ({ ); }; -Button.kinds = kinds; -Button.displays = displays; +Button.kinds = Kinds; +Button.displays = Displays; export default Button; From 7718e02dcd1c7c6951bc566abc1b4dd76c646b6d Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Tue, 19 Aug 2025 22:15:30 +0100 Subject: [PATCH 28/49] SkipLink: update type to interface --- client/components/SkipLink.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/components/SkipLink.tsx b/client/components/SkipLink.tsx index c5b7b15e57..06d11ed0ea 100644 --- a/client/components/SkipLink.tsx +++ b/client/components/SkipLink.tsx @@ -2,10 +2,10 @@ import React, { useState } from 'react'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; -type SkipLinkProps = { +interface SkipLinkProps { targetId: string; text: string; -}; +} const SkipLink = ({ targetId, text }: SkipLinkProps) => { const [focus, setFocus] = useState(false); From 3428dacb75698d79bcb92d35c199ed3d98dc971f Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Thu, 14 Aug 2025 19:51:45 +0100 Subject: [PATCH 29/49] .eslintrc: cherry-pick ts overrids from utils pr --- .eslintrc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.eslintrc b/.eslintrc index 7371a6155a..d8f24cf68d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -41,6 +41,7 @@ "no-promise-executor-return": 0, //temporarily off "no-restricted-exports": 1, "no-underscore-dangle": 0, + "no-underscore-dangle": 0, "no-useless-catch": 2, "prefer-object-spread": 0, "max-len": [1, 120, 2, {"ignoreComments": true, "ignoreTemplateLiterals": true}], @@ -133,6 +134,8 @@ "no-use-before-define": "off", "import/no-extraneous-dependencies": "off", "no-unused-vars": "off", + "import/no-default-export": "warn", + "no-underscore-dangle": "warn", "react/require-default-props": "off", "no-shadow": "off", "@typescript-eslint/no-shadow": "error" From 1a7cfaab579382c4dae444f60d86fb99b20e12fb Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Wed, 20 Aug 2025 00:42:28 +0100 Subject: [PATCH 30/49] useSyncFormTranslations: update to be named export --- client/common/useSyncFormTranslations.test.tsx | 2 +- client/common/useSyncFormTranslations.ts | 4 +--- client/modules/User/components/LoginForm.jsx | 2 +- client/modules/User/components/LoginForm.unit.test.jsx | 4 +++- client/modules/User/components/SignupForm.jsx | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/common/useSyncFormTranslations.test.tsx b/client/common/useSyncFormTranslations.test.tsx index 63622de2e0..3dbaf663bf 100644 --- a/client/common/useSyncFormTranslations.test.tsx +++ b/client/common/useSyncFormTranslations.test.tsx @@ -1,6 +1,6 @@ import React, { useRef, useState, useEffect } from 'react'; import { render } from '@testing-library/react'; -import useSyncFormTranslations from './useSyncFormTranslations'; +import { useSyncFormTranslations } from './useSyncFormTranslations'; describe('useSyncFormTranslations', () => { it('resets and re-applies non-empty form values on language change', () => { diff --git a/client/common/useSyncFormTranslations.ts b/client/common/useSyncFormTranslations.ts index 0a3ed46725..4a90362750 100644 --- a/client/common/useSyncFormTranslations.ts +++ b/client/common/useSyncFormTranslations.ts @@ -11,7 +11,7 @@ export interface FormLike { * @param formRef * @param language */ -const useSyncFormTranslations = ( +export const useSyncFormTranslations = ( formRef: MutableRefObject, language: string ) => { @@ -29,5 +29,3 @@ const useSyncFormTranslations = ( }); }, [language]); }; - -export default useSyncFormTranslations; diff --git a/client/modules/User/components/LoginForm.jsx b/client/modules/User/components/LoginForm.jsx index 6d5d6a1b2e..e90840209d 100644 --- a/client/modules/User/components/LoginForm.jsx +++ b/client/modules/User/components/LoginForm.jsx @@ -6,7 +6,7 @@ import { AiOutlineEye, AiOutlineEyeInvisible } from 'react-icons/ai'; import Button from '../../../common/Button'; import { validateLogin } from '../../../utils/reduxFormUtils'; import { validateAndLoginUser } from '../actions'; -import useSyncFormTranslations from '../../../common/useSyncFormTranslations'; +import { useSyncFormTranslations } from '../../../common/useSyncFormTranslations'; function LoginForm() { const { t, i18n } = useTranslation(); diff --git a/client/modules/User/components/LoginForm.unit.test.jsx b/client/modules/User/components/LoginForm.unit.test.jsx index 7ab207fb01..69764931dd 100644 --- a/client/modules/User/components/LoginForm.unit.test.jsx +++ b/client/modules/User/components/LoginForm.unit.test.jsx @@ -23,7 +23,9 @@ jest.mock('../actions', () => ({ ) })); -jest.mock('../../../common/useSyncFormTranslations', () => jest.fn()); +jest.mock('../../../common/useSyncFormTranslations', () => ({ + useSyncFormTranslations: jest.fn(), +})); const subject = () => { reduxRender(, { diff --git a/client/modules/User/components/SignupForm.jsx b/client/modules/User/components/SignupForm.jsx index d3e6129461..d3b5c190f0 100644 --- a/client/modules/User/components/SignupForm.jsx +++ b/client/modules/User/components/SignupForm.jsx @@ -7,7 +7,7 @@ import { validateSignup } from '../../../utils/reduxFormUtils'; import { validateAndSignUpUser } from '../actions'; import Button from '../../../common/Button'; import apiClient from '../../../utils/apiClient'; -import useSyncFormTranslations from '../../../common/useSyncFormTranslations'; +import {useSyncFormTranslations} from '../../../common/useSyncFormTranslations'; const timeoutRef = { current: null }; From 3676534135947e367d6bd1468f2906ff720f3fba Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Wed, 20 Aug 2025 00:43:24 +0100 Subject: [PATCH 31/49] eslintrc: fix double config --- .eslintrc | 1 - 1 file changed, 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index d8f24cf68d..9bc88882fe 100644 --- a/.eslintrc +++ b/.eslintrc @@ -41,7 +41,6 @@ "no-promise-executor-return": 0, //temporarily off "no-restricted-exports": 1, "no-underscore-dangle": 0, - "no-underscore-dangle": 0, "no-useless-catch": 2, "prefer-object-spread": 0, "max-len": [1, 120, 2, {"ignoreComments": true, "ignoreTemplateLiterals": true}], From 366aa7ec0e7f9172ce11dca4f6f596d1a3a4eab9 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Wed, 20 Aug 2025 00:45:41 +0100 Subject: [PATCH 32/49] usePrevious: update to be named export --- client/common/usePrevious.test.tsx | 2 +- client/common/usePrevious.ts | 2 +- client/components/Menubar/Menubar.jsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/common/usePrevious.test.tsx b/client/common/usePrevious.test.tsx index 13a1731feb..1ff89904d3 100644 --- a/client/common/usePrevious.test.tsx +++ b/client/common/usePrevious.test.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { render, screen } from '../test-utils'; -import usePrevious from './usePrevious'; +import { usePrevious } from './usePrevious'; function TestComponent({ value }: { value: number }) { const prev = usePrevious(value); diff --git a/client/common/usePrevious.ts b/client/common/usePrevious.ts index 2919694db5..34f18edff6 100644 --- a/client/common/usePrevious.ts +++ b/client/common/usePrevious.ts @@ -5,7 +5,7 @@ import { useEffect, useRef } from 'react'; * @param value - The current value to track. * @returns The previous value before the current render, or undefined if none. */ -export default function usePrevious(value: unknown): unknown | undefined { +export function usePrevious(value: unknown): unknown | undefined { const ref = useRef(); useEffect(() => { diff --git a/client/components/Menubar/Menubar.jsx b/client/components/Menubar/Menubar.jsx index 8a358fceb6..8a748dbfc6 100644 --- a/client/components/Menubar/Menubar.jsx +++ b/client/components/Menubar/Menubar.jsx @@ -8,7 +8,7 @@ import React, { } from 'react'; import useModalClose from '../../common/useModalClose'; import { MenuOpenContext, MenubarContext } from './contexts'; -import usePrevious from '../../common/usePrevious'; +import { usePrevious } from '../../common/usePrevious'; /** * Menubar manages a collection of menu items and their submenus. It provides keyboard navigation, From 2d8fe065a17a74af14ed15493ebe92538a7fadbe Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Wed, 20 Aug 2025 00:51:32 +0100 Subject: [PATCH 33/49] useModalClose: update to named export --- client/common/useModalClose.test.tsx | 2 +- client/common/useModalClose.ts | 2 +- client/components/Dropdown/DropdownMenu.jsx | 2 +- client/components/Menubar/Menubar.jsx | 2 +- client/modules/App/components/Overlay.jsx | 2 +- client/modules/IDE/components/Modal.jsx | 2 +- client/modules/User/components/CollectionShareButton.jsx | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/common/useModalClose.test.tsx b/client/common/useModalClose.test.tsx index 64d26f7308..a754d81553 100644 --- a/client/common/useModalClose.test.tsx +++ b/client/common/useModalClose.test.tsx @@ -1,6 +1,6 @@ import React, { useRef, useEffect } from 'react'; import { render, fireEvent } from '@testing-library/react'; -import useModalClose from './useModalClose'; +import { useModalClose } from './useModalClose'; import useKeyDownHandlers from './useKeyDownHandlers'; jest.mock('./useKeyDownHandlers'); diff --git a/client/common/useModalClose.ts b/client/common/useModalClose.ts index cd19da2b04..fabd3e83ce 100644 --- a/client/common/useModalClose.ts +++ b/client/common/useModalClose.ts @@ -18,7 +18,7 @@ import useKeyDownHandlers from './useKeyDownHandlers'; * @param passedRef - Optional ref to the modal element. If not provided, one is created internally. * @returns A ref to be attached to the modal DOM element */ -export default function useModalClose( +export function useModalClose( onClose: () => void, passedRef?: MutableRefObject ): MutableRefObject { diff --git a/client/components/Dropdown/DropdownMenu.jsx b/client/components/Dropdown/DropdownMenu.jsx index ddaa1c338a..a760db01c2 100644 --- a/client/components/Dropdown/DropdownMenu.jsx +++ b/client/components/Dropdown/DropdownMenu.jsx @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React, { forwardRef, useCallback, useRef, useState } from 'react'; -import useModalClose from '../../common/useModalClose'; +import { useModalClose } from '../../common/useModalClose'; import DownArrowIcon from '../../images/down-filled-triangle.svg'; import { DropdownWrapper } from '../Dropdown'; diff --git a/client/components/Menubar/Menubar.jsx b/client/components/Menubar/Menubar.jsx index 8a748dbfc6..3cceea48bb 100644 --- a/client/components/Menubar/Menubar.jsx +++ b/client/components/Menubar/Menubar.jsx @@ -6,7 +6,7 @@ import React, { useState, useEffect } from 'react'; -import useModalClose from '../../common/useModalClose'; +import { useModalClose } from '../../common/useModalClose'; import { MenuOpenContext, MenubarContext } from './contexts'; import { usePrevious } from '../../common/usePrevious'; diff --git a/client/modules/App/components/Overlay.jsx b/client/modules/App/components/Overlay.jsx index 2dedc82656..6ae400add7 100644 --- a/client/modules/App/components/Overlay.jsx +++ b/client/modules/App/components/Overlay.jsx @@ -4,7 +4,7 @@ import MediaQuery from 'react-responsive'; import { useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import useModalClose from '../../../common/useModalClose'; +import { useModalClose } from '../../../common/useModalClose'; import ExitIcon from '../../../images/exit.svg'; diff --git a/client/modules/IDE/components/Modal.jsx b/client/modules/IDE/components/Modal.jsx index 831527b266..c3456373c0 100644 --- a/client/modules/IDE/components/Modal.jsx +++ b/client/modules/IDE/components/Modal.jsx @@ -1,7 +1,7 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; -import useModalClose from '../../../common/useModalClose'; +import { useModalClose } from '../../../common/useModalClose'; import ExitIcon from '../../../images/exit.svg'; // Common logic from NewFolderModal, NewFileModal, UploadFileModal diff --git a/client/modules/User/components/CollectionShareButton.jsx b/client/modules/User/components/CollectionShareButton.jsx index c4fd06bcb6..4daa5a8544 100644 --- a/client/modules/User/components/CollectionShareButton.jsx +++ b/client/modules/User/components/CollectionShareButton.jsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import Button from '../../../common/Button'; import { DropdownArrowIcon } from '../../../common/icons'; -import useModalClose from '../../../common/useModalClose'; +import { useModalClose } from '../../../common/useModalClose'; import CopyableInput from '../../IDE/components/CopyableInput'; const ShareURL = ({ value }) => { From b1788d5f7406db672b0ee72b083ebfd7a9c6d358 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Wed, 20 Aug 2025 00:55:24 +0100 Subject: [PATCH 34/49] useKeyDownHandler: update to named export --- client/common/useKeyDownHandlers.test.tsx | 2 +- client/common/useKeyDownHandlers.ts | 2 +- client/common/useModalClose.test.tsx | 2 +- client/common/useModalClose.ts | 2 +- client/modules/IDE/components/IDEKeyHandlers.jsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/common/useKeyDownHandlers.test.tsx b/client/common/useKeyDownHandlers.test.tsx index f1d3480d96..b776d609ae 100644 --- a/client/common/useKeyDownHandlers.test.tsx +++ b/client/common/useKeyDownHandlers.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { render } from '@testing-library/react'; -import useKeyDownHandlers from './useKeyDownHandlers'; +import { useKeyDownHandlers } from './useKeyDownHandlers'; import { isMac } from '../utils/device'; jest.mock('../utils/device'); diff --git a/client/common/useKeyDownHandlers.ts b/client/common/useKeyDownHandlers.ts index 2c8da5a3eb..21c61cdfe9 100644 --- a/client/common/useKeyDownHandlers.ts +++ b/client/common/useKeyDownHandlers.ts @@ -13,7 +13,7 @@ export type KeydownHandlerMap = Record; * @param keyHandlers - an object which maps from the key to its event handler. The object keys are a combination of the key and prefixes `ctrl-` `shift-` * (ie. 'ctrl-f', 'ctrl-shift-f') and the values are the function to call when that specific key is pressed. */ -export default function useKeyDownHandlers(keyHandlers: KeydownHandlerMap) { +export function useKeyDownHandlers(keyHandlers: KeydownHandlerMap) { /** * Instead of memoizing the handlers, use a ref and call the current * handler at the time of the event. diff --git a/client/common/useModalClose.test.tsx b/client/common/useModalClose.test.tsx index a754d81553..8c5fd36613 100644 --- a/client/common/useModalClose.test.tsx +++ b/client/common/useModalClose.test.tsx @@ -1,7 +1,7 @@ import React, { useRef, useEffect } from 'react'; import { render, fireEvent } from '@testing-library/react'; import { useModalClose } from './useModalClose'; -import useKeyDownHandlers from './useKeyDownHandlers'; +import { useKeyDownHandlers } from './useKeyDownHandlers'; jest.mock('./useKeyDownHandlers'); diff --git a/client/common/useModalClose.ts b/client/common/useModalClose.ts index fabd3e83ce..de106a2c0f 100644 --- a/client/common/useModalClose.ts +++ b/client/common/useModalClose.ts @@ -1,5 +1,5 @@ import { useEffect, useRef, MutableRefObject } from 'react'; -import useKeyDownHandlers from './useKeyDownHandlers'; +import { useKeyDownHandlers } from './useKeyDownHandlers'; /** * Common logic for Modal, Overlay, etc. diff --git a/client/modules/IDE/components/IDEKeyHandlers.jsx b/client/modules/IDE/components/IDEKeyHandlers.jsx index fc0219f27d..14e3cf2cba 100644 --- a/client/modules/IDE/components/IDEKeyHandlers.jsx +++ b/client/modules/IDE/components/IDEKeyHandlers.jsx @@ -13,7 +13,7 @@ import { } from '../actions/ide'; import { setAllAccessibleOutput } from '../actions/preferences'; import { cloneProject, saveProject } from '../actions/project'; -import useKeyDownHandlers from '../../../common/useKeyDownHandlers'; +import { useKeyDownHandlers } from '../../../common/useKeyDownHandlers'; import { getAuthenticated, getIsUserOwner, From bc82be58911e2edd10945d1d5a1fa27206f8178a Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Wed, 20 Aug 2025 00:59:04 +0100 Subject: [PATCH 35/49] RouterTab: update to named export --- client/common/RouterTab.test.tsx | 6 +++--- client/common/RouterTab.tsx | 6 ++---- client/modules/Legal/pages/Legal.jsx | 2 +- client/modules/User/components/DashboardTabSwitcher.jsx | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/client/common/RouterTab.test.tsx b/client/common/RouterTab.test.tsx index 95f451ffef..b9652b6a37 100644 --- a/client/common/RouterTab.test.tsx +++ b/client/common/RouterTab.test.tsx @@ -1,13 +1,13 @@ import React from 'react'; import { render, screen, fireEvent, waitFor, history } from '../test-utils'; -import Tab from './RouterTab'; +import { RouterTab } from './RouterTab'; const mockPath = '/projects'; const mockLinkText = 'Projects'; -describe('Tab', () => { +describe('RouterTab', () => { function mountComponent() { - return render({mockLinkText}); + return render({mockLinkText}); } it('renders a react-router NavLink with correct text and path', async () => { diff --git a/client/common/RouterTab.tsx b/client/common/RouterTab.tsx index 571cf5ddd2..aa4f9aee92 100644 --- a/client/common/RouterTab.tsx +++ b/client/common/RouterTab.tsx @@ -1,7 +1,7 @@ import React, { ReactNode } from 'react'; import { NavLink } from 'react-router-dom'; -export interface TabProps { +export interface RouterTabProps { children: ReactNode; to: string; } @@ -9,7 +9,7 @@ export interface TabProps { /** * Wraps the react-router `NavLink` with dashboard-header__tab styling. */ -const Tab = ({ children, to }: TabProps) => ( +export const RouterTab = ({ children, to }: RouterTabProps) => ( ( ); - -export default Tab; diff --git a/client/modules/Legal/pages/Legal.jsx b/client/modules/Legal/pages/Legal.jsx index 0e629f789a..24f8920fe3 100644 --- a/client/modules/Legal/pages/Legal.jsx +++ b/client/modules/Legal/pages/Legal.jsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react'; import Helmet from 'react-helmet'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; -import RouterTab from '../../../common/RouterTab'; +import { RouterTab } from '../../../common/RouterTab'; import RootPage from '../../../components/RootPage'; import { remSize } from '../../../theme'; import Loader from '../../App/components/loader'; diff --git a/client/modules/User/components/DashboardTabSwitcher.jsx b/client/modules/User/components/DashboardTabSwitcher.jsx index 6bfdb9bb22..f55af669b3 100644 --- a/client/modules/User/components/DashboardTabSwitcher.jsx +++ b/client/modules/User/components/DashboardTabSwitcher.jsx @@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { FilterIcon } from '../../../common/icons'; import IconButton from '../../../common/IconButton'; -import RouterTab from '../../../common/RouterTab'; +import { RouterTab } from '../../../common/RouterTab'; import { Options } from '../../IDE/components/Header/MobileNav'; import { toggleDirectionForField } from '../../IDE/actions/sorting'; import useIsMobile from '../../IDE/hooks/useIsMobile'; From 7b67549b5da470346660e29f353794f1cc3616e0 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Wed, 20 Aug 2025 01:03:23 +0100 Subject: [PATCH 36/49] ButtonOrLink: update to named export --- client/common/ButtonOrLink.test.tsx | 2 +- client/common/ButtonOrLink.tsx | 4 +--- client/components/Dropdown/MenuItem.jsx | 2 +- client/components/Menubar/MenubarItem.jsx | 2 +- client/modules/IDE/components/Header/MobileNav.jsx | 2 +- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/client/common/ButtonOrLink.test.tsx b/client/common/ButtonOrLink.test.tsx index 7b1a6326a4..c2dfb37d10 100644 --- a/client/common/ButtonOrLink.test.tsx +++ b/client/common/ButtonOrLink.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { render, screen, fireEvent, waitFor, history } from '../test-utils'; -import ButtonOrLink from './ButtonOrLink'; +import { ButtonOrLink } from './ButtonOrLink'; describe('ButtonOrLink', () => { const clickHandler = jest.fn(); diff --git a/client/common/ButtonOrLink.tsx b/client/common/ButtonOrLink.tsx index ac5bc7ff4a..6ae5e2187c 100644 --- a/client/common/ButtonOrLink.tsx +++ b/client/common/ButtonOrLink.tsx @@ -28,7 +28,7 @@ export type Ref = HTMLAnchorElement | HTMLButtonElement; * - Internal links will use react-router. * - External links should start with 'http' or 'https' and will open in a new window. */ -const ButtonOrLink = React.forwardRef( +export const ButtonOrLink = React.forwardRef( ( { href, @@ -92,5 +92,3 @@ const ButtonOrLink = React.forwardRef( ); } ); - -export default ButtonOrLink; diff --git a/client/components/Dropdown/MenuItem.jsx b/client/components/Dropdown/MenuItem.jsx index a7908e487d..79349c00cc 100644 --- a/client/components/Dropdown/MenuItem.jsx +++ b/client/components/Dropdown/MenuItem.jsx @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import ButtonOrLink from '../../common/ButtonOrLink'; +import { ButtonOrLink } from '../../common/ButtonOrLink'; // TODO: combine with NavMenuItem diff --git a/client/components/Menubar/MenubarItem.jsx b/client/components/Menubar/MenubarItem.jsx index ab3b741e34..27e5b1f7c2 100644 --- a/client/components/Menubar/MenubarItem.jsx +++ b/client/components/Menubar/MenubarItem.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React, { useEffect, useContext, useRef } from 'react'; import { MenubarContext, SubmenuContext, ParentMenuContext } from './contexts'; -import ButtonOrLink from '../../common/ButtonOrLink'; +import { ButtonOrLink } from '../../common/ButtonOrLink'; /** * MenubarItem wraps a button or link in an accessible list item that diff --git a/client/modules/IDE/components/Header/MobileNav.jsx b/client/modules/IDE/components/Header/MobileNav.jsx index dfb0209c1b..b060037d60 100644 --- a/client/modules/IDE/components/Header/MobileNav.jsx +++ b/client/modules/IDE/components/Header/MobileNav.jsx @@ -8,7 +8,7 @@ import classNames from 'classnames'; import { ParentMenuContext } from '../../../../components/Menubar/contexts'; import Menubar from '../../../../components/Menubar/Menubar'; import { useMenuProps } from '../../../../components/Menubar/MenubarSubmenu'; -import ButtonOrLink from '../../../../common/ButtonOrLink'; +import { ButtonOrLink } from '../../../../common/ButtonOrLink'; import { prop, remSize } from '../../../../theme'; import AsteriskIcon from '../../../../images/p5-asterisk.svg'; import IconButton from '../../../../common/IconButton'; From 379e56361985683311962de5637bad6fefba4dbf Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Wed, 20 Aug 2025 01:10:23 +0100 Subject: [PATCH 37/49] IconButton: update to interface and named export --- client/common/IconButton.test.tsx | 2 +- client/common/IconButton.tsx | 12 ++++-------- client/components/Dropdown.jsx | 2 +- client/modules/IDE/components/Editor/index.jsx | 2 +- client/modules/IDE/components/Header/MobileNav.jsx | 2 +- .../modules/User/components/DashboardTabSwitcher.jsx | 2 +- 6 files changed, 9 insertions(+), 13 deletions(-) diff --git a/client/common/IconButton.test.tsx b/client/common/IconButton.test.tsx index 811a795184..5ddaf854ba 100644 --- a/client/common/IconButton.test.tsx +++ b/client/common/IconButton.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { render, screen } from '../test-utils'; -import IconButton from './IconButton'; +import { IconButton } from './IconButton'; const MockIcon = (props: React.SVGProps) => ( diff --git a/client/common/IconButton.tsx b/client/common/IconButton.tsx index 977a362dc4..9aaa8cae6f 100644 --- a/client/common/IconButton.tsx +++ b/client/common/IconButton.tsx @@ -11,14 +11,12 @@ const ButtonWrapper = styled(Button)` } `; -export type IconButtonProps = Omit< - ButtonProps, - 'iconBefore' | 'display' | 'focusable' -> & { +export interface IconButtonProps + extends Omit { icon?: ComponentType<{ 'aria-label'?: string }>; -}; +} -const IconButton = ({ icon: Icon, ...otherProps }: IconButtonProps) => ( +export const IconButton = ({ icon: Icon, ...otherProps }: IconButtonProps) => ( : undefined} iconOnly @@ -27,5 +25,3 @@ const IconButton = ({ icon: Icon, ...otherProps }: IconButtonProps) => ( {...otherProps} /> ); - -export default IconButton; diff --git a/client/components/Dropdown.jsx b/client/components/Dropdown.jsx index c04f961d75..57b52c59e0 100644 --- a/client/components/Dropdown.jsx +++ b/client/components/Dropdown.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; import { remSize, prop } from '../theme'; -import IconButton from '../common/IconButton'; +import { IconButton } from '../common/IconButton'; export const DropdownWrapper = styled.ul` background-color: ${prop('Modal.background')}; diff --git a/client/modules/IDE/components/Editor/index.jsx b/client/modules/IDE/components/Editor/index.jsx index 76bf2f03f2..4e4b7c784d 100644 --- a/client/modules/IDE/components/Editor/index.jsx +++ b/client/modules/IDE/components/Editor/index.jsx @@ -71,7 +71,7 @@ import EditorAccessibility from '../EditorAccessibility'; import UnsavedChangesIndicator from '../UnsavedChangesIndicator'; import { EditorContainer, EditorHolder } from './MobileEditor'; import { FolderIcon } from '../../../../common/icons'; -import IconButton from '../../../../common/IconButton'; +import { IconButton } from '../../../../common/IconButton'; emmet(CodeMirror); diff --git a/client/modules/IDE/components/Header/MobileNav.jsx b/client/modules/IDE/components/Header/MobileNav.jsx index b060037d60..3ff659e606 100644 --- a/client/modules/IDE/components/Header/MobileNav.jsx +++ b/client/modules/IDE/components/Header/MobileNav.jsx @@ -11,7 +11,7 @@ import { useMenuProps } from '../../../../components/Menubar/MenubarSubmenu'; import { ButtonOrLink } from '../../../../common/ButtonOrLink'; import { prop, remSize } from '../../../../theme'; import AsteriskIcon from '../../../../images/p5-asterisk.svg'; -import IconButton from '../../../../common/IconButton'; +import { IconButton } from '../../../../common/IconButton'; import { AccountIcon, AddIcon, diff --git a/client/modules/User/components/DashboardTabSwitcher.jsx b/client/modules/User/components/DashboardTabSwitcher.jsx index f55af669b3..7afbe5aec8 100644 --- a/client/modules/User/components/DashboardTabSwitcher.jsx +++ b/client/modules/User/components/DashboardTabSwitcher.jsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { FilterIcon } from '../../../common/icons'; -import IconButton from '../../../common/IconButton'; +import { IconButton } from '../../../common/IconButton'; import { RouterTab } from '../../../common/RouterTab'; import { Options } from '../../IDE/components/Header/MobileNav'; import { toggleDirectionForField } from '../../IDE/actions/sorting'; From 95152915d23204da9f07bb2633fc691130a5c819 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 25 Aug 2025 13:05:12 +0100 Subject: [PATCH 38/49] Button: update to named export --- client/common/Button.stories.jsx | 2 +- client/common/Button.test.tsx | 2 +- client/common/Button.tsx | 4 +--- client/common/IconButton.tsx | 2 +- client/modules/IDE/components/NewFileForm.jsx | 2 +- client/modules/IDE/components/NewFolderForm.jsx | 2 +- client/modules/User/components/APIKeyForm.jsx | 2 +- client/modules/User/components/AccountForm.jsx | 2 +- client/modules/User/components/CollectionCreate.jsx | 2 +- client/modules/User/components/CollectionMetadata.jsx | 2 +- client/modules/User/components/CollectionShareButton.jsx | 2 +- client/modules/User/components/CookieConsent.jsx | 2 +- client/modules/User/components/LoginForm.jsx | 2 +- client/modules/User/components/NewPasswordForm.jsx | 2 +- client/modules/User/components/ResetPasswordForm.jsx | 2 +- client/modules/User/components/SignupForm.jsx | 4 ++-- client/modules/User/components/SocialAuthButton.jsx | 2 +- client/modules/User/pages/DashboardView.jsx | 2 +- 18 files changed, 19 insertions(+), 21 deletions(-) diff --git a/client/common/Button.stories.jsx b/client/common/Button.stories.jsx index d11634ae28..d869e7f6bd 100644 --- a/client/common/Button.stories.jsx +++ b/client/common/Button.stories.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { action } from '@storybook/addon-actions'; -import Button from './Button'; +import { Button } from './Button'; import { GithubIcon, DropdownArrowIcon, PlusIcon } from './icons'; export default { diff --git a/client/common/Button.test.tsx b/client/common/Button.test.tsx index 863f2d89b2..b49e40c78f 100644 --- a/client/common/Button.test.tsx +++ b/client/common/Button.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { render, screen, fireEvent } from '../test-utils'; -import Button from './Button'; +import { Button } from './Button'; const MockIcon = (props: React.SVGProps) => ( diff --git a/client/common/Button.tsx b/client/common/Button.tsx index b7825846c9..ab27670766 100644 --- a/client/common/Button.tsx +++ b/client/common/Button.tsx @@ -178,7 +178,7 @@ const StyledInlineButton = styled.button` /** * A Button performs an primary action */ -const Button = ({ +export const Button = ({ children = null, display = Displays.block, href, @@ -248,5 +248,3 @@ const Button = ({ Button.kinds = Kinds; Button.displays = Displays; - -export default Button; diff --git a/client/common/IconButton.tsx b/client/common/IconButton.tsx index 9aaa8cae6f..9860c3e8c2 100644 --- a/client/common/IconButton.tsx +++ b/client/common/IconButton.tsx @@ -1,6 +1,6 @@ import React, { ComponentType } from 'react'; import styled from 'styled-components'; -import Button, { ButtonProps } from './Button'; +import { Button, ButtonProps } from './Button'; import { remSize } from '../theme'; const ButtonWrapper = styled(Button)` diff --git a/client/modules/IDE/components/NewFileForm.jsx b/client/modules/IDE/components/NewFileForm.jsx index 9620e321dc..eb4822cf1e 100644 --- a/client/modules/IDE/components/NewFileForm.jsx +++ b/client/modules/IDE/components/NewFileForm.jsx @@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux'; import { handleCreateFile } from '../actions/files'; import { CREATE_FILE_REGEX } from '../../../../server/utils/fileUtils'; -import Button from '../../../common/Button'; +import { Button } from '../../../common/Button'; function NewFileForm() { const fileNameInput = useRef(null); diff --git a/client/modules/IDE/components/NewFolderForm.jsx b/client/modules/IDE/components/NewFolderForm.jsx index b0935397ff..a5afd60ff8 100644 --- a/client/modules/IDE/components/NewFolderForm.jsx +++ b/client/modules/IDE/components/NewFolderForm.jsx @@ -2,7 +2,7 @@ import React, { useRef, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Form, Field } from 'react-final-form'; import { useDispatch } from 'react-redux'; -import Button from '../../../common/Button'; +import { Button } from '../../../common/Button'; import { handleCreateFolder } from '../actions/files'; function NewFolderForm() { diff --git a/client/modules/User/components/APIKeyForm.jsx b/client/modules/User/components/APIKeyForm.jsx index eff0ae2d7b..379f5e19d4 100644 --- a/client/modules/User/components/APIKeyForm.jsx +++ b/client/modules/User/components/APIKeyForm.jsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; -import Button from '../../../common/Button'; +import { Button } from '../../../common/Button'; import { PlusIcon } from '../../../common/icons'; import CopyableInput from '../../IDE/components/CopyableInput'; import { createApiKey, removeApiKey } from '../actions'; diff --git a/client/modules/User/components/AccountForm.jsx b/client/modules/User/components/AccountForm.jsx index 4406d58c17..6a944bd093 100644 --- a/client/modules/User/components/AccountForm.jsx +++ b/client/modules/User/components/AccountForm.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { Form, Field } from 'react-final-form'; import { useSelector, useDispatch } from 'react-redux'; import { useTranslation } from 'react-i18next'; -import Button from '../../../common/Button'; +import { Button } from '../../../common/Button'; import { validateSettings } from '../../../utils/reduxFormUtils'; import { updateSettings, initiateVerification } from '../actions'; import apiClient from '../../../utils/apiClient'; diff --git a/client/modules/User/components/CollectionCreate.jsx b/client/modules/User/components/CollectionCreate.jsx index 02d3fb59f0..8b89d3d7db 100644 --- a/client/modules/User/components/CollectionCreate.jsx +++ b/client/modules/User/components/CollectionCreate.jsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { generateCollectionName } from '../../../utils/generateRandomName'; -import Button from '../../../common/Button'; +import { Button } from '../../../common/Button'; import { createCollection } from '../../IDE/actions/collections'; const CollectionCreate = () => { diff --git a/client/modules/User/components/CollectionMetadata.jsx b/client/modules/User/components/CollectionMetadata.jsx index 88dc6d0312..dcb47e0929 100644 --- a/client/modules/User/components/CollectionMetadata.jsx +++ b/client/modules/User/components/CollectionMetadata.jsx @@ -4,7 +4,7 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { Link } from 'react-router-dom'; -import Button from '../../../common/Button'; +import { Button } from '../../../common/Button'; import Overlay from '../../App/components/Overlay'; import { editCollection } from '../../IDE/actions/collections'; import AddToCollectionSketchList from '../../IDE/components/AddToCollectionSketchList'; diff --git a/client/modules/User/components/CollectionShareButton.jsx b/client/modules/User/components/CollectionShareButton.jsx index 4daa5a8544..a5d8705dcd 100644 --- a/client/modules/User/components/CollectionShareButton.jsx +++ b/client/modules/User/components/CollectionShareButton.jsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React, { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import Button from '../../../common/Button'; +import { Button } from '../../../common/Button'; import { DropdownArrowIcon } from '../../../common/icons'; import { useModalClose } from '../../../common/useModalClose'; import CopyableInput from '../../IDE/components/CopyableInput'; diff --git a/client/modules/User/components/CookieConsent.jsx b/client/modules/User/components/CookieConsent.jsx index 37b2e0618f..4cedfb8c24 100644 --- a/client/modules/User/components/CookieConsent.jsx +++ b/client/modules/User/components/CookieConsent.jsx @@ -10,7 +10,7 @@ import PropTypes from 'prop-types'; import getConfig from '../../../utils/getConfig'; import { setUserCookieConsent } from '../actions'; import { remSize, prop, device } from '../../../theme'; -import Button from '../../../common/Button'; +import { Button } from '../../../common/Button'; const CookieConsentContainer = styled.div` position: fixed; diff --git a/client/modules/User/components/LoginForm.jsx b/client/modules/User/components/LoginForm.jsx index e90840209d..7170f0944c 100644 --- a/client/modules/User/components/LoginForm.jsx +++ b/client/modules/User/components/LoginForm.jsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { Form, Field } from 'react-final-form'; import { useDispatch } from 'react-redux'; import { AiOutlineEye, AiOutlineEyeInvisible } from 'react-icons/ai'; -import Button from '../../../common/Button'; +import { Button } from '../../../common/Button'; import { validateLogin } from '../../../utils/reduxFormUtils'; import { validateAndLoginUser } from '../actions'; import { useSyncFormTranslations } from '../../../common/useSyncFormTranslations'; diff --git a/client/modules/User/components/NewPasswordForm.jsx b/client/modules/User/components/NewPasswordForm.jsx index 95e96ec4a3..2404ae4cd1 100644 --- a/client/modules/User/components/NewPasswordForm.jsx +++ b/client/modules/User/components/NewPasswordForm.jsx @@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { validateNewPassword } from '../../../utils/reduxFormUtils'; import { updatePassword } from '../actions'; -import Button from '../../../common/Button'; +import { Button } from '../../../common/Button'; function NewPasswordForm(props) { const { resetPasswordToken } = props; diff --git a/client/modules/User/components/ResetPasswordForm.jsx b/client/modules/User/components/ResetPasswordForm.jsx index f44e7649bd..6f7a45b1ba 100644 --- a/client/modules/User/components/ResetPasswordForm.jsx +++ b/client/modules/User/components/ResetPasswordForm.jsx @@ -4,7 +4,7 @@ import { Form, Field } from 'react-final-form'; import { useDispatch, useSelector } from 'react-redux'; import { validateResetPassword } from '../../../utils/reduxFormUtils'; import { initiateResetPassword } from '../actions'; -import Button from '../../../common/Button'; +import { Button } from '../../../common/Button'; function ResetPasswordForm(props) { const { t } = useTranslation(); diff --git a/client/modules/User/components/SignupForm.jsx b/client/modules/User/components/SignupForm.jsx index d3b5c190f0..f4abdf55ae 100644 --- a/client/modules/User/components/SignupForm.jsx +++ b/client/modules/User/components/SignupForm.jsx @@ -5,9 +5,9 @@ import { useDispatch } from 'react-redux'; import { AiOutlineEye, AiOutlineEyeInvisible } from 'react-icons/ai'; import { validateSignup } from '../../../utils/reduxFormUtils'; import { validateAndSignUpUser } from '../actions'; -import Button from '../../../common/Button'; +import { Button } from '../../../common/Button'; import apiClient from '../../../utils/apiClient'; -import {useSyncFormTranslations} from '../../../common/useSyncFormTranslations'; +import { useSyncFormTranslations } from '../../../common/useSyncFormTranslations'; const timeoutRef = { current: null }; diff --git a/client/modules/User/components/SocialAuthButton.jsx b/client/modules/User/components/SocialAuthButton.jsx index ac4a8b3b11..e48c660b67 100644 --- a/client/modules/User/components/SocialAuthButton.jsx +++ b/client/modules/User/components/SocialAuthButton.jsx @@ -6,7 +6,7 @@ import { useDispatch } from 'react-redux'; import { remSize } from '../../../theme'; import { GithubIcon, GoogleIcon } from '../../../common/icons'; -import Button from '../../../common/Button'; +import { Button } from '../../../common/Button'; import { unlinkService } from '../actions'; import { persistState } from '../../IDE/actions/project'; diff --git a/client/modules/User/pages/DashboardView.jsx b/client/modules/User/pages/DashboardView.jsx index fcde949cd7..611fef9f5a 100644 --- a/client/modules/User/pages/DashboardView.jsx +++ b/client/modules/User/pages/DashboardView.jsx @@ -3,7 +3,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { useLocation, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import Button from '../../../common/Button'; +import { Button } from '../../../common/Button'; import Nav from '../../IDE/components/Header/Nav'; import Overlay from '../../App/components/Overlay'; import AssetList from '../../IDE/components/AssetList'; From dc7665d08ff499fa3a3661af32ae152fe8aa21f9 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 25 Aug 2025 13:58:23 +0100 Subject: [PATCH 39/49] Button: clean up type definition to interface and add basic html element attributes --- client/common/Button.tsx | 25 +++++++++++++------------ client/common/IconButton.tsx | 3 +++ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/client/common/Button.tsx b/client/common/Button.tsx index ab27670766..f1760fd915 100644 --- a/client/common/Button.tsx +++ b/client/common/Button.tsx @@ -1,6 +1,6 @@ import React from 'react'; import styled from 'styled-components'; -import { Link, LinkProps } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import { remSize, prop } from '../theme'; enum Kinds { @@ -18,13 +18,7 @@ enum ButtonTypes { submit = 'submit' } -type StyledButtonProps = { - kind: ButtonTypes; - display: Displays; - type?: ButtonTypes; -} & React.ButtonHTMLAttributes; - -interface SharedButtonProps { +export interface ButtonProps extends React.HTMLAttributes { /** * The visible part of the button, telling the user what * the action is @@ -66,6 +60,10 @@ interface SharedButtonProps { * Specifying a to URL will use a react-router Link */ to?: string; + /** + * If using a native button, specifies on an onClick action + */ + onClick?: () => void; /** * If using a button, then type is defines the type of button */ @@ -77,10 +75,10 @@ interface SharedButtonProps { focusable?: boolean | 'true' | 'false'; } -export type ButtonProps = SharedButtonProps & - React.ButtonHTMLAttributes & - React.AnchorHTMLAttributes & - Partial; +interface StyledButtonProps extends ButtonProps { + kind: Kinds; + display: Displays; +} // The '&&&' will increase the specificity of the // component's CSS so that it overrides the more @@ -203,6 +201,7 @@ export const Button = ({ ? StyledInlineButton : StyledButton; + // Anchor Link if (href) { return ( { icon?: ComponentType<{ 'aria-label'?: string }>; + href?: string; + to?: string; + onClick?: () => void; } export const IconButton = ({ icon: Icon, ...otherProps }: IconButtonProps) => ( From 0ff9ac1192bc8b850ea4d130f9668998073db82e Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 25 Aug 2025 13:59:20 +0100 Subject: [PATCH 40/49] lint --- client/modules/User/components/LoginForm.unit.test.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/modules/User/components/LoginForm.unit.test.jsx b/client/modules/User/components/LoginForm.unit.test.jsx index 69764931dd..2f4262c16b 100644 --- a/client/modules/User/components/LoginForm.unit.test.jsx +++ b/client/modules/User/components/LoginForm.unit.test.jsx @@ -24,7 +24,7 @@ jest.mock('../actions', () => ({ })); jest.mock('../../../common/useSyncFormTranslations', () => ({ - useSyncFormTranslations: jest.fn(), + useSyncFormTranslations: jest.fn() })); const subject = () => { From 67425a1a99a6d5951babfeb0bcd23e7ea1d36857 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Tue, 19 Aug 2025 22:15:30 +0100 Subject: [PATCH 41/49] Button: update jsdoc --- client/common/Button.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/common/Button.tsx b/client/common/Button.tsx index f1760fd915..db21e3f354 100644 --- a/client/common/Button.tsx +++ b/client/common/Button.tsx @@ -174,7 +174,10 @@ const StyledInlineButton = styled.button` `; /** - * A Button performs an primary action + * Renders a component with a button appearance, but which is: + * - External anchor link if passed a `href` + * - Internal React Router link if passed a `to` + * - Default: Native Button */ export const Button = ({ children = null, From 001c9e44a6d7819bed980280a21793c10d11184c Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Tue, 19 Aug 2025 22:38:11 +0100 Subject: [PATCH 42/49] SkipLink: Update to named export --- client/components/SkipLink.test.tsx | 2 +- client/components/SkipLink.tsx | 4 +--- client/index.jsx | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/client/components/SkipLink.test.tsx b/client/components/SkipLink.test.tsx index ba6d78517d..692a08a616 100644 --- a/client/components/SkipLink.test.tsx +++ b/client/components/SkipLink.test.tsx @@ -2,7 +2,7 @@ import { render, screen, fireEvent } from '@testing-library/react'; import React from 'react'; import { useTranslation } from 'react-i18next'; import '@testing-library/jest-dom'; -import SkipLink from './SkipLink'; +import { SkipLink } from './SkipLink'; jest.mock('react-i18next', () => ({ useTranslation: () => ({ diff --git a/client/components/SkipLink.tsx b/client/components/SkipLink.tsx index 06d11ed0ea..28b02b8b5c 100644 --- a/client/components/SkipLink.tsx +++ b/client/components/SkipLink.tsx @@ -7,7 +7,7 @@ interface SkipLinkProps { text: string; } -const SkipLink = ({ targetId, text }: SkipLinkProps) => { +export const SkipLink = ({ targetId, text }: SkipLinkProps) => { const [focus, setFocus] = useState(false); const { t } = useTranslation(); const handleFocus = () => { @@ -30,5 +30,3 @@ const SkipLink = ({ targetId, text }: SkipLinkProps) => { ); }; - -export default SkipLink; diff --git a/client/index.jsx b/client/index.jsx index 27befe8421..0c1ca04082 100644 --- a/client/index.jsx +++ b/client/index.jsx @@ -9,7 +9,7 @@ import Routing from './routes'; import ThemeProvider from './modules/App/components/ThemeProvider'; import Loader from './modules/App/components/loader'; import './i18n'; -import SkipLink from './components/SkipLink'; +import { SkipLink } from './components/SkipLink'; require('./styles/main.scss'); From e8ba7b83f6843370fe56c1214fe851378c78cdd4 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 25 Aug 2025 15:51:54 +0100 Subject: [PATCH 43/49] SkipLink: update test setup to use testutils --- client/components/SkipLink.test.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/client/components/SkipLink.test.tsx b/client/components/SkipLink.test.tsx index 692a08a616..8cf2e40052 100644 --- a/client/components/SkipLink.test.tsx +++ b/client/components/SkipLink.test.tsx @@ -1,15 +1,7 @@ -import { render, screen, fireEvent } from '@testing-library/react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; -import '@testing-library/jest-dom'; +import { render, screen, fireEvent } from '../test-utils'; import { SkipLink } from './SkipLink'; -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key - }) -})); - describe('SkipLink', () => { const defaultProps = { targetId: 'main-content', From 6e411066a410544a036bae6da807922f143aef06 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 25 Aug 2025 15:54:54 +0100 Subject: [PATCH 44/49] useKeyDownHandlers: update to use testutils --- client/common/useKeyDownHandlers.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/common/useKeyDownHandlers.test.tsx b/client/common/useKeyDownHandlers.test.tsx index b776d609ae..3b3f023c10 100644 --- a/client/common/useKeyDownHandlers.test.tsx +++ b/client/common/useKeyDownHandlers.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { render } from '../test-utils'; import { useKeyDownHandlers } from './useKeyDownHandlers'; import { isMac } from '../utils/device'; From 3d5fba385dfeae7e2fb49207461201da13c3af91 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 25 Aug 2025 15:57:36 +0100 Subject: [PATCH 45/49] useModalClose: update to use testutils --- client/common/useModalClose.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/common/useModalClose.test.tsx b/client/common/useModalClose.test.tsx index 8c5fd36613..252f78b61e 100644 --- a/client/common/useModalClose.test.tsx +++ b/client/common/useModalClose.test.tsx @@ -1,5 +1,5 @@ -import React, { useRef, useEffect } from 'react'; -import { render, fireEvent } from '@testing-library/react'; +import React from 'react'; +import { render, fireEvent } from '../test-utils'; import { useModalClose } from './useModalClose'; import { useKeyDownHandlers } from './useKeyDownHandlers'; From 316637e2236f0100401f42337709ac4b5141a672 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 25 Aug 2025 15:58:13 +0100 Subject: [PATCH 46/49] usePrevious: update to use testutils --- client/common/usePrevious.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/common/usePrevious.test.tsx b/client/common/usePrevious.test.tsx index 1ff89904d3..35f6e6c001 100644 --- a/client/common/usePrevious.test.tsx +++ b/client/common/usePrevious.test.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { render, screen } from '../test-utils'; import { usePrevious } from './usePrevious'; From 3e89f7f2159e2e3ddc6b2457f0ee51ff6f79249d Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 25 Aug 2025 15:59:01 +0100 Subject: [PATCH 47/49] useSyncFormTranslations: update to use testutils --- client/common/useSyncFormTranslations.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/common/useSyncFormTranslations.test.tsx b/client/common/useSyncFormTranslations.test.tsx index 3dbaf663bf..67c90d114f 100644 --- a/client/common/useSyncFormTranslations.test.tsx +++ b/client/common/useSyncFormTranslations.test.tsx @@ -1,5 +1,5 @@ -import React, { useRef, useState, useEffect } from 'react'; -import { render } from '@testing-library/react'; +import React, { useRef } from 'react'; +import { render } from '../test-utils'; import { useSyncFormTranslations } from './useSyncFormTranslations'; describe('useSyncFormTranslations', () => { From 8f0f0b5f68d761b7f24723d0dfa48f514a377095 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Thu, 28 Aug 2025 20:03:54 +0100 Subject: [PATCH 48/49] Button & IconButton: update focusable to be boolean not booleanish string --- client/common/Button.tsx | 8 +++++--- client/common/IconButton.tsx | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/client/common/Button.tsx b/client/common/Button.tsx index db21e3f354..0b557f4e1b 100644 --- a/client/common/Button.tsx +++ b/client/common/Button.tsx @@ -69,10 +69,12 @@ export interface ButtonProps extends React.HTMLAttributes { */ type?: ButtonTypes; /** - * Allows for IconButton to pass `focusable="false"` as a prop for SVGs. - * See @types/react > interface SVGAttributes extends AriaAttributes, DOMAttributes + * Controls whether the underlying SVG is focusable. + * Only relevant for IconButton (or buttons that render an SVG as content). + * In SVGs, the `focusable` attribute must be a string (`"true"` or `"false"`), + * but React will automatically convert a boolean prop to the correct string value. */ - focusable?: boolean | 'true' | 'false'; + focusable?: boolean; } interface StyledButtonProps extends ButtonProps { diff --git a/client/common/IconButton.tsx b/client/common/IconButton.tsx index 96faf3ac1d..b9aba250c2 100644 --- a/client/common/IconButton.tsx +++ b/client/common/IconButton.tsx @@ -24,7 +24,7 @@ export const IconButton = ({ icon: Icon, ...otherProps }: IconButtonProps) => ( iconBefore={Icon ? : undefined} iconOnly display={Button.displays.inline} - focusable="false" + focusable={false} {...otherProps} /> ); From 627462ea5dd34addb4cb3f2dcce5508c646d82b8 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Thu, 28 Aug 2025 20:44:07 +0100 Subject: [PATCH 49/49] Button: update enum to explicit export and in constant case --- client/common/Button.stories.jsx | 10 ++-- client/common/Button.tsx | 58 ++++++++++++------- client/common/IconButton.tsx | 4 +- client/modules/IDE/components/NewFileForm.jsx | 4 +- .../modules/IDE/components/NewFolderForm.jsx | 4 +- client/modules/User/components/APIKeyForm.jsx | 4 +- .../modules/User/components/AccountForm.jsx | 4 +- .../User/components/CollectionCreate.jsx | 4 +- .../modules/User/components/CookieConsent.jsx | 7 +-- client/modules/User/components/LoginForm.jsx | 4 +- .../User/components/NewPasswordForm.jsx | 7 ++- .../User/components/ResetPasswordForm.jsx | 4 +- client/modules/User/components/SignupForm.jsx | 7 ++- 13 files changed, 69 insertions(+), 52 deletions(-) diff --git a/client/common/Button.stories.jsx b/client/common/Button.stories.jsx index d869e7f6bd..0a0150a5b6 100644 --- a/client/common/Button.stories.jsx +++ b/client/common/Button.stories.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { action } from '@storybook/addon-actions'; -import { Button } from './Button'; +import { Button, ButtonDisplays, ButtonKinds, ButtonTypes } from './Button'; import { GithubIcon, DropdownArrowIcon, PlusIcon } from './icons'; export default { @@ -15,13 +15,13 @@ export default { }; export const AllFeatures = (args) => ( - + {args.children} ); export const SubmitButton = () => ( - + This is a submit button ); @@ -59,7 +59,7 @@ export const ButtonWithIconAfter = () => ( ); export const InlineButtonWithIconAfter = () => ( - } display={Button.displays.inline}> + } display={ButtonDisplays.INLINE}> File name ); @@ -68,6 +68,6 @@ export const InlineIconOnlyButton = () => ( } - display={Button.displays.inline} + display={ButtonDisplays.INLINE} /> ); diff --git a/client/common/Button.tsx b/client/common/Button.tsx index 0b557f4e1b..d52abf3140 100644 --- a/client/common/Button.tsx +++ b/client/common/Button.tsx @@ -3,19 +3,36 @@ import styled from 'styled-components'; import { Link } from 'react-router-dom'; import { remSize, prop } from '../theme'; -enum Kinds { - primary = 'primary', - secondary = 'secondary' +/** + * Enum for the visual style of a Button. + * + * These values transpile to lowercase strings (`'primary' | 'secondary'`) + * that map directly to keys in the `Button` object in `theme.js` for styling. + */ +export enum ButtonKinds { + PRIMARY = 'primary', + SECONDARY = 'secondary' } - -enum Displays { - block = 'block', - inline = 'inline' +/** + * Enum for the display type of a Button. + * + * These values transpile to lowercase strings (`'block' | 'inline'`) + * and map to display styles in the `Button` object in `theme.js`. + */ +export enum ButtonDisplays { + BLOCK = 'block', + INLINE = 'inline' } - -enum ButtonTypes { - button = 'button', - submit = 'submit' +/** + * Enum for the native HTML button type. + * + * These values transpile to lowercase strings (`'button' | 'submit'`) + * and correspond to the `type` attribute on a native . + * They can also be used in `theme.js` if needed for button-specific styles. + */ +export enum ButtonTypes { + BUTTON = 'button', + SUBMIT = 'submit' } export interface ButtonProps extends React.HTMLAttributes { @@ -31,7 +48,7 @@ export interface ButtonProps extends React.HTMLAttributes { /** * The display type of the button—inline or block */ - display?: Displays; + display?: ButtonDisplays; /** * SVG icon to place after child content */ @@ -47,7 +64,7 @@ export interface ButtonProps extends React.HTMLAttributes { /** * The kind of button - determines how it appears visually */ - kind?: Kinds; + kind?: ButtonKinds; /** * Specifying an href will use an to link to the URL */ @@ -78,8 +95,8 @@ export interface ButtonProps extends React.HTMLAttributes { } interface StyledButtonProps extends ButtonProps { - kind: Kinds; - display: Displays; + kind: ButtonKinds; + display: ButtonDisplays; } // The '&&&' will increase the specificity of the @@ -89,7 +106,7 @@ const StyledButton = styled.button` &&& { font-weight: bold; display: ${({ display }) => - display === Displays.inline ? 'inline-flex' : 'flex'}; + display === ButtonDisplays.INLINE ? 'inline-flex' : 'flex'}; justify-content: center; align-items: center; @@ -183,15 +200,15 @@ const StyledInlineButton = styled.button` */ export const Button = ({ children = null, - display = Displays.block, + display = ButtonDisplays.BLOCK, href, - kind = Kinds.primary, + kind = ButtonKinds.PRIMARY, iconBefore = null, iconAfter = null, iconOnly = false, 'aria-label': ariaLabel, to, - type = ButtonTypes.button, + type = ButtonTypes.BUTTON, ...props }: ButtonProps) => { const hasChildren = React.Children.count(children) > 0; @@ -251,6 +268,3 @@ export const Button = ({ ); }; - -Button.kinds = Kinds; -Button.displays = Displays; diff --git a/client/common/IconButton.tsx b/client/common/IconButton.tsx index b9aba250c2..22c7620606 100644 --- a/client/common/IconButton.tsx +++ b/client/common/IconButton.tsx @@ -1,6 +1,6 @@ import React, { ComponentType } from 'react'; import styled from 'styled-components'; -import { Button, ButtonProps } from './Button'; +import { Button, ButtonProps, ButtonDisplays } from './Button'; import { remSize } from '../theme'; const ButtonWrapper = styled(Button)` @@ -23,7 +23,7 @@ export const IconButton = ({ icon: Icon, ...otherProps }: IconButtonProps) => ( : undefined} iconOnly - display={Button.displays.inline} + display={ButtonDisplays.INLINE} focusable={false} {...otherProps} /> diff --git a/client/modules/IDE/components/NewFileForm.jsx b/client/modules/IDE/components/NewFileForm.jsx index eb4822cf1e..dda19e343c 100644 --- a/client/modules/IDE/components/NewFileForm.jsx +++ b/client/modules/IDE/components/NewFileForm.jsx @@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux'; import { handleCreateFile } from '../actions/files'; import { CREATE_FILE_REGEX } from '../../../../server/utils/fileUtils'; -import { Button } from '../../../common/Button'; +import { Button, ButtonTypes } from '../../../common/Button'; function NewFileForm() { const fileNameInput = useRef(null); @@ -57,7 +57,7 @@ function NewFileForm() { {() => ( - + {t('NewFileForm.AddFileSubmit')} )} diff --git a/client/modules/IDE/components/NewFolderForm.jsx b/client/modules/IDE/components/NewFolderForm.jsx index a5afd60ff8..381520c47e 100644 --- a/client/modules/IDE/components/NewFolderForm.jsx +++ b/client/modules/IDE/components/NewFolderForm.jsx @@ -2,7 +2,7 @@ import React, { useRef, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Form, Field } from 'react-final-form'; import { useDispatch } from 'react-redux'; -import { Button } from '../../../common/Button'; +import { Button, ButtonTypes } from '../../../common/Button'; import { handleCreateFolder } from '../actions/files'; function NewFolderForm() { @@ -54,7 +54,7 @@ function NewFolderForm() { {() => ( - + {t('NewFolderForm.AddFolderSubmit')} )} diff --git a/client/modules/User/components/APIKeyForm.jsx b/client/modules/User/components/APIKeyForm.jsx index 379f5e19d4..fda5945f87 100644 --- a/client/modules/User/components/APIKeyForm.jsx +++ b/client/modules/User/components/APIKeyForm.jsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; -import { Button } from '../../../common/Button'; +import { Button, ButtonTypes } from '../../../common/Button'; import { PlusIcon } from '../../../common/icons'; import CopyableInput from '../../IDE/components/CopyableInput'; import { createApiKey, removeApiKey } from '../actions'; @@ -78,7 +78,7 @@ const APIKeyForm = () => { disabled={keyLabel === ''} iconBefore={} label="Create new key" - type="submit" + type={ButtonTypes.SUBMIT} > {t('APIKeyForm.CreateTokenSubmit')} diff --git a/client/modules/User/components/AccountForm.jsx b/client/modules/User/components/AccountForm.jsx index a4905e081f..4ef40e4298 100644 --- a/client/modules/User/components/AccountForm.jsx +++ b/client/modules/User/components/AccountForm.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { Form, Field } from 'react-final-form'; import { useSelector, useDispatch } from 'react-redux'; import { useTranslation } from 'react-i18next'; -import { Button } from '../../../common/Button'; +import { Button, ButtonTypes } from '../../../common/Button'; import { validateSettings } from '../../../utils/reduxFormUtils'; import { updateSettings, initiateVerification } from '../actions'; import { apiClient } from '../../../utils/apiClient'; @@ -175,7 +175,7 @@ function AccountForm() { )} )} - + {t('AccountForm.SaveAccountDetails')} diff --git a/client/modules/User/components/CollectionCreate.jsx b/client/modules/User/components/CollectionCreate.jsx index 8b89d3d7db..3b2ccdefed 100644 --- a/client/modules/User/components/CollectionCreate.jsx +++ b/client/modules/User/components/CollectionCreate.jsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { generateCollectionName } from '../../../utils/generateRandomName'; -import { Button } from '../../../common/Button'; +import { Button, ButtonTypes } from '../../../common/Button'; import { createCollection } from '../../IDE/actions/collections'; const CollectionCreate = () => { @@ -74,7 +74,7 @@ const CollectionCreate = () => { rows="6" />