From f1af0a6e8c4acf1f8dd1c80cec53cd547db6913f Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Sun, 3 Aug 2025 10:41:49 +0100 Subject: [PATCH 01/76] .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/76] 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/76] 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/76] 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/76] 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/76] 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/76] 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(); + 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(); + const link = screen.getByRole('link'); + expect(link.tagName.toLowerCase()).toBe('a'); // Link renders as + expect(link).toHaveAttribute('href', '/dashboard'); + }); + + it('renders as a ); + const el = screen.getByRole('button'); + expect(el.tagName.toLowerCase()).toBe('button'); + expect(el).toHaveAttribute('type', 'button'); + }); + + // Children & Icons + it('renders children', () => { + render(); + expect(screen.getByText('Click Me')).toBeInTheDocument(); + }); + + it('renders an iconBefore and button text', () => { + render( + + ); + expect(screen.getByLabelText('iconbefore')).toBeInTheDocument(); + expect(screen.getByRole('button')).toHaveTextContent( + 'This has a before icon' + ); + }); + + it('renders with iconAfter', () => { + render( + + ); + expect(screen.getByLabelText('iconafter')).toBeInTheDocument(); + expect(screen.getByRole('button')).toHaveTextContent( + 'This has an after icon' + ); + }); + + it('renders only the icon if iconOnly', () => { + render( + + ); + expect(screen.getByLabelText('iconafter')).toBeInTheDocument(); + expect(screen.getByRole('button')).not.toHaveTextContent( + 'This has an after icon' + ); + }); + + // HTML attributes + it('calls onClick handler when clicked', () => { + const handleClick = jest.fn(); + render(); + fireEvent.click(screen.getByText('Click')); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it('renders disabled state', () => { + render(); + expect(screen.getByRole('button')).toBeDisabled(); + }); + + it('uses aria-label when provided', () => { + render( {isOpen && ( - {children} - + )} ); From f050cfc6f3e7a08933eb2dbc68e009655debe708 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 25 Aug 2025 16:26:42 +0100 Subject: [PATCH 55/76] DropdownWrapper: Delete file, only styled dropdown wrapper is used in the repo. styled dropdown wrapper is directly migrated to DropdownMenu instead --- .../components/Dropdown/DropdownWrapper.tsx | 110 ------------------ 1 file changed, 110 deletions(-) delete mode 100644 client/components/Dropdown/DropdownWrapper.tsx diff --git a/client/components/Dropdown/DropdownWrapper.tsx b/client/components/Dropdown/DropdownWrapper.tsx deleted file mode 100644 index 15bf015b71..0000000000 --- a/client/components/Dropdown/DropdownWrapper.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import styled from 'styled-components'; -import { remSize, prop } from '../../theme'; -import { IconButton } from '../../common/IconButton'; - -export const DropdownWrapper = styled.ul` - background-color: ${prop('Modal.background')}; - border: 1px solid ${prop('Modal.border')}; - box-shadow: 0 0 18px 0 ${prop('shadowColor')}; - color: ${prop('primaryTextColor')}; - - position: absolute; - right: ${(props) => (props.right ? 0 : 'initial')}; - left: ${(props) => (props.left ? 0 : 'initial')}; - - ${(props) => props.align === 'right' && 'right: 0;'} - ${(props) => props.align === 'left' && 'left: 0;'} - - - text-align: left; - width: ${remSize(180)}; - display: flex; - flex-direction: column; - height: auto; - z-index: 2; - border-radius: ${remSize(6)}; - - & li:first-child { - border-radius: ${remSize(5)} ${remSize(5)} 0 0; - } - & li:last-child { - border-radius: 0 0 ${remSize(5)} ${remSize(5)}; - } - - & li:hover { - background-color: ${prop('Button.primary.hover.background')}; - color: ${prop('Button.primary.hover.foreground')}; - - * { - color: ${prop('Button.primary.hover.foreground')}; - } - } - - li { - height: ${remSize(36)}; - cursor: pointer; - display: flex; - align-items: center; - - & button, - & button span, - & a { - padding: ${remSize(8)} ${remSize(16)}; - font-size: ${remSize(12)}; - } - - * { - text-align: left; - justify-content: left; - - color: ${prop('primaryTextColor')}; - width: 100%; - justify-content: flex-start; - } - - & button span { - padding: 0px; - } - } -`; - -// TODO: Add Icon to the left of the items in the menu -// const MaybeIcon = (Element, label) => Element && ; - -const Dropdown = ({ items, align }) => ( - - {/* className="nav__items-left" */} - {items && - items.map(({ title, icon, href, action }) => ( -
  • - {/* {MaybeIcon(icon, `Navigate to ${title}`)} */} - {href ? ( - {title} - ) : ( - action()}>{title} - )} -
  • - ))} -
    -); - -Dropdown.propTypes = { - align: PropTypes.oneOf(['left', 'right']), - items: PropTypes.arrayOf( - PropTypes.shape({ - action: PropTypes.func, - icon: PropTypes.func, - href: PropTypes.string, - title: PropTypes.string - }) - ) -}; - -Dropdown.defaultProps = { - items: [], - align: null -}; - -export default Dropdown; From fc85d2925e2955adb26498fa32b61b85a94793ec Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 25 Aug 2025 16:28:26 +0100 Subject: [PATCH 56/76] MenuItem: migrate to tsx --no-verify --- client/components/Dropdown/{MenuItem.jsx => MenuItem.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/components/Dropdown/{MenuItem.jsx => MenuItem.tsx} (100%) diff --git a/client/components/Dropdown/MenuItem.jsx b/client/components/Dropdown/MenuItem.tsx similarity index 100% rename from client/components/Dropdown/MenuItem.jsx rename to client/components/Dropdown/MenuItem.tsx From 1b6ad1f8aec4feb2720fb77c853100b7593cf22b Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 25 Aug 2025 16:34:30 +0100 Subject: [PATCH 57/76] MenuItem: add interface, update to named export --- client/components/Dropdown/MenuItem.tsx | 31 ++++++------------- .../modules/IDE/components/AssetListRow.jsx | 2 +- .../CollectionList/CollectionListRow.jsx | 2 +- .../IDE/components/SketchListRowBase.jsx | 2 +- .../modules/IDE/components/VersionPicker.jsx | 2 +- 5 files changed, 14 insertions(+), 25 deletions(-) diff --git a/client/components/Dropdown/MenuItem.tsx b/client/components/Dropdown/MenuItem.tsx index 79349c00cc..401aae3361 100644 --- a/client/components/Dropdown/MenuItem.tsx +++ b/client/components/Dropdown/MenuItem.tsx @@ -1,10 +1,17 @@ -import PropTypes from 'prop-types'; import React from 'react'; -import { ButtonOrLink } from '../../common/ButtonOrLink'; +import { ButtonOrLink, ButtonOrLinkProps } from '../../common/ButtonOrLink'; // TODO: combine with NavMenuItem -function MenuItem({ hideIf, ...rest }) { +export interface MenuItemProps extends ButtonOrLinkProps { + /** + * Provides a way to deal with optional items. + */ + hideIf?: boolean; + value?: string; +} + +export function MenuItem({ hideIf = false, ...rest }: MenuItemProps) { if (hideIf) { return null; } @@ -15,21 +22,3 @@ function MenuItem({ hideIf, ...rest }) { ); } - -MenuItem.propTypes = { - ...ButtonOrLink.propTypes, - onClick: PropTypes.func, - value: PropTypes.string, - /** - * Provides a way to deal with optional items. - */ - hideIf: PropTypes.bool -}; - -MenuItem.defaultProps = { - onClick: null, - value: null, - hideIf: false -}; - -export default MenuItem; diff --git a/client/modules/IDE/components/AssetListRow.jsx b/client/modules/IDE/components/AssetListRow.jsx index 7e7af8f013..90667edb45 100644 --- a/client/modules/IDE/components/AssetListRow.jsx +++ b/client/modules/IDE/components/AssetListRow.jsx @@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'; import { useDispatch } from 'react-redux'; import { useTranslation } from 'react-i18next'; import prettyBytes from 'pretty-bytes'; -import MenuItem from '../../../components/Dropdown/MenuItem'; +import { MenuItem } from '../../../components/Dropdown/MenuItem'; import TableDropdown from '../../../components/Dropdown/TableDropdown'; import { deleteAssetRequest } from '../actions/assets'; diff --git a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx index a2096fc788..aca4f38014 100644 --- a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx @@ -5,7 +5,7 @@ import { Link } from 'react-router-dom'; import { bindActionCreators } from 'redux'; import { withTranslation } from 'react-i18next'; import styled from 'styled-components'; -import MenuItem from '../../../../components/Dropdown/MenuItem'; +import { MenuItem } from '../../../../components/Dropdown/MenuItem'; import TableDropdown from '../../../../components/Dropdown/TableDropdown'; import * as ProjectActions from '../../actions/project'; import * as CollectionsActions from '../../actions/collections'; diff --git a/client/modules/IDE/components/SketchListRowBase.jsx b/client/modules/IDE/components/SketchListRowBase.jsx index b50cf63e4d..1b39fb50a9 100644 --- a/client/modules/IDE/components/SketchListRowBase.jsx +++ b/client/modules/IDE/components/SketchListRowBase.jsx @@ -7,7 +7,7 @@ import { connect } from 'react-redux'; import * as ProjectActions from '../actions/project'; import * as IdeActions from '../actions/ide'; import TableDropdown from '../../../components/Dropdown/TableDropdown'; -import MenuItem from '../../../components/Dropdown/MenuItem'; +import { MenuItem } from '../../../components/Dropdown/MenuItem'; import dates from '../../../utils/formatDate'; import getConfig from '../../../utils/getConfig'; import VisibilityDropdown from '../../User/components/VisibilityDropdown'; diff --git a/client/modules/IDE/components/VersionPicker.jsx b/client/modules/IDE/components/VersionPicker.jsx index cd891d020c..8400882146 100644 --- a/client/modules/IDE/components/VersionPicker.jsx +++ b/client/modules/IDE/components/VersionPicker.jsx @@ -7,7 +7,7 @@ import styled from 'styled-components'; import { prop } from '../../../theme'; import { useP5Version } from '../hooks/useP5Version'; import { p5Versions } from '../../../../common/p5Versions'; -import MenuItem from '../../../components/Dropdown/MenuItem'; +import { MenuItem } from '../../../components/Dropdown/MenuItem'; import DropdownMenu from '../../../components/Dropdown/DropdownMenu'; import { updateFileContent } from '../actions/files'; // eslint-disable-next-line import/no-cycle From 01cb10e6a554ddaa0b719b90bc7dda4c4ff3625a Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 25 Aug 2025 16:39:28 +0100 Subject: [PATCH 58/76] ButtonOrLink: add basic html element attributes as optional props --- client/common/ButtonOrLink.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/common/ButtonOrLink.tsx b/client/common/ButtonOrLink.tsx index 6ae5e2187c..ce1ac5f1ed 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 ); export const SubmitButton = () => ( - ); @@ -59,7 +59,7 @@ export const ButtonWithIconAfter = () => ( ); export const InlineButtonWithIconAfter = () => ( - ); @@ -68,6 +68,6 @@ export const InlineIconOnlyButton = () => ( )} 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() { {() => ( - )} 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() { )} )} - 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" />

    - diff --git a/client/modules/User/components/CookieConsent.jsx b/client/modules/User/components/CookieConsent.jsx index 18811c84a5..9cfff74ac6 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, ButtonKinds } from '../../../common/Button'; const CookieConsentContainer = styled.div` position: fixed; @@ -177,10 +177,7 @@ function CookieConsent({ hide }) { /> - diff --git a/client/modules/User/components/NewPasswordForm.jsx b/client/modules/User/components/NewPasswordForm.jsx index 2404ae4cd1..feca326c77 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, ButtonTypes } from '../../../common/Button'; function NewPasswordForm(props) { const { resetPasswordToken } = props; @@ -64,7 +64,10 @@ function NewPasswordForm(props) {

    )} - diff --git a/client/modules/User/components/ResetPasswordForm.jsx b/client/modules/User/components/ResetPasswordForm.jsx index 6f7a45b1ba..fe3752fdf4 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, ButtonTypes } from '../../../common/Button'; function ResetPasswordForm(props) { const { t } = useTranslation(); @@ -45,7 +45,7 @@ function ResetPasswordForm(props) { )} From 51b69d9ebf90afd232ba7d5816b3eafbce5ed3d8 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Thu, 28 Aug 2025 21:51:47 +0100 Subject: [PATCH 71/76] MenuBar: fix ts errors from keydownHandler and useModalClose --- client/common/useKeyDownHandlers.ts | 4 ++-- client/common/useModalClose.ts | 8 ++++---- client/components/Menubar/Menubar.tsx | 5 ++--- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/client/common/useKeyDownHandlers.ts b/client/common/useKeyDownHandlers.ts index 21c61cdfe9..b1f51bb7af 100644 --- a/client/common/useKeyDownHandlers.ts +++ b/client/common/useKeyDownHandlers.ts @@ -1,9 +1,9 @@ import { mapKeys } from 'lodash'; -import { useCallback, useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import { isMac } from '../utils/device'; /** Function to call upon keydown */ -export type KeydownHandler = (e: KeyboardEvent) => void; +export type KeydownHandler = (e: KeyboardEvent | React.KeyboardEvent) => void; /** An object mapping from keys like 'ctrl-s' or 'ctrl-shift-1' to handlers. */ export type KeydownHandlerMap = Record; diff --git a/client/common/useModalClose.ts b/client/common/useModalClose.ts index de106a2c0f..044cd5d190 100644 --- a/client/common/useModalClose.ts +++ b/client/common/useModalClose.ts @@ -18,11 +18,11 @@ 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 function useModalClose( +export function useModalClose( onClose: () => void, - passedRef?: MutableRefObject -): MutableRefObject { - const createdRef = useRef(null); + passedRef?: MutableRefObject +): MutableRefObject { + const createdRef = useRef(null); const modalRef = passedRef ?? createdRef; useEffect(() => { diff --git a/client/components/Menubar/Menubar.tsx b/client/components/Menubar/Menubar.tsx index 702092d72e..f7a6a6d0a8 100644 --- a/client/components/Menubar/Menubar.tsx +++ b/client/components/Menubar/Menubar.tsx @@ -4,8 +4,7 @@ import React, { useRef, useState, useEffect, - MouseEvent, - KeyboardEvent + MouseEvent } from 'react'; import { useModalClose } from '../../common/useModalClose'; import { MenuOpenContext, MenubarContext } from './contexts'; @@ -125,7 +124,7 @@ export function Menubar({ setMenuOpen('none'); }, [setMenuOpen]); - const nodeRef = useModalClose(handleClose); + const nodeRef = useModalClose(handleClose); const handleFocus = useCallback(() => { setHasFocus(true); From 936238c5e7e28478799fd1a5ec5e2a02285de8e2 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Thu, 28 Aug 2025 21:58:02 +0100 Subject: [PATCH 72/76] MenuBar: test update to tsx --- client/components/Menubar/{Menubar.test.jsx => Menubar.test.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/components/Menubar/{Menubar.test.jsx => Menubar.test.tsx} (100%) diff --git a/client/components/Menubar/Menubar.test.jsx b/client/components/Menubar/Menubar.test.tsx similarity index 100% rename from client/components/Menubar/Menubar.test.jsx rename to client/components/Menubar/Menubar.test.tsx From 9b1e574172782377c4d03e6ba145b824bd2ebc1b Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Thu, 28 Aug 2025 21:59:48 +0100 Subject: [PATCH 73/76] MenubarSubmenu: update to tsx, no-verify --- .../components/Menubar/{MenubarSubmenu.jsx => MenubarSubmenu.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/components/Menubar/{MenubarSubmenu.jsx => MenubarSubmenu.tsx} (100%) diff --git a/client/components/Menubar/MenubarSubmenu.jsx b/client/components/Menubar/MenubarSubmenu.tsx similarity index 100% rename from client/components/Menubar/MenubarSubmenu.jsx rename to client/components/Menubar/MenubarSubmenu.tsx From 50c28218c214b71a82d45d99155037aefc31f9c3 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 1 Sep 2025 15:11:52 +0100 Subject: [PATCH 74/76] MenubarSubmenu: wip - update context related type errors, and update menuBarSubMenu related useages -- no-verify --- client/components/Dropdown/DropdownMenu.tsx | 2 +- client/components/Menubar/Menubar.test.tsx | 2 +- client/components/Menubar/MenubarSubmenu.tsx | 231 ++++++++---------- client/components/Menubar/contexts.tsx | 35 ++- client/modules/IDE/components/Header/Nav.jsx | 2 +- .../IDE/components/Header/Nav.unit.test.jsx | 45 ++-- 6 files changed, 168 insertions(+), 149 deletions(-) diff --git a/client/components/Dropdown/DropdownMenu.tsx b/client/components/Dropdown/DropdownMenu.tsx index 1411f21ea9..258f681832 100644 --- a/client/components/Dropdown/DropdownMenu.tsx +++ b/client/components/Dropdown/DropdownMenu.tsx @@ -108,7 +108,7 @@ export const DropdownMenu = forwardRef( const close = useCallback(() => setIsOpen(false), [setIsOpen]); - const anchorRef = useModalClose(close, ref); + const anchorRef = useModalClose(close, ref); const toggle = useCallback(() => { setIsOpen((prevState) => !prevState); diff --git a/client/components/Menubar/Menubar.test.tsx b/client/components/Menubar/Menubar.test.tsx index 3f5b3871fc..ade21a4138 100644 --- a/client/components/Menubar/Menubar.test.tsx +++ b/client/components/Menubar/Menubar.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render, screen, fireEvent } from '../../test-utils'; import { Menubar } from './Menubar'; -import MenubarSubmenu from './MenubarSubmenu'; +import { MenubarSubmenu } from './MenubarSubmenu'; import { MenubarItem } from './MenubarItem'; describe('Menubar', () => { diff --git a/client/components/Menubar/MenubarSubmenu.tsx b/client/components/Menubar/MenubarSubmenu.tsx index 38683f2310..4f9f51c032 100644 --- a/client/components/Menubar/MenubarSubmenu.tsx +++ b/client/components/Menubar/MenubarSubmenu.tsx @@ -1,7 +1,6 @@ // https://blog.logrocket.com/building-accessible-menubar-component-react import classNames from 'classnames'; -import PropTypes from 'prop-types'; import React, { useState, useEffect, @@ -18,11 +17,12 @@ import { } from './contexts'; import TriangleIcon from '../../images/down-filled-triangle.svg'; -export function useMenuProps(id) { +/* ------------------------------------------------------------------------------------------------- + * useMenuProps + * -----------------------------------------------------------------------------------------------*/ +export function useMenuProps(id: string) { const activeMenu = useContext(MenuOpenContext); - const isOpen = id === activeMenu; - const { createMenuHandlers } = useContext(MenubarContext); const handlers = useMemo(() => createMenuHandlers(id), [ @@ -36,7 +36,11 @@ export function useMenuProps(id) { /* ------------------------------------------------------------------------------------------------- * MenubarTrigger * -----------------------------------------------------------------------------------------------*/ - +interface MenubarTriggerProps + extends React.ButtonHTMLAttributes { + role?: string; + hasPopup?: 'menu' | 'listbox' | 'true'; +} /** * MenubarTrigger renders a button that toggles a submenu. It handles keyboard navigation and supports * screen readers. It needs to be within a submenu context. @@ -63,111 +67,107 @@ export function useMenuProps(id) { * */ -const MenubarTrigger = React.forwardRef(({ role, hasPopup, ...props }, ref) => { - const { - setActiveIndex, - menuItems, - registerTopLevelItem, - hasFocus - } = useContext(MenubarContext); - const { id, title, first, last } = useContext(SubmenuContext); - const { isOpen, handlers } = useMenuProps(id); - - const handleMouseEnter = () => { - if (hasFocus) { - const items = Array.from(menuItems); - const index = items.findIndex((item) => item === ref.current); - - if (index !== -1) { - setActiveIndex(index); - } - } - }; - - const handleKeyDown = (e) => { - switch (e.key) { - case 'ArrowDown': - if (!isOpen) { - e.preventDefault(); - e.stopPropagation(); - first(); - } - break; - case 'ArrowUp': - if (!isOpen) { - e.preventDefault(); - e.stopPropagation(); - last(); +const MenubarTrigger = React.forwardRef( + ( + { role = 'menuitem', hasPopup = 'menu', ...props }: MenubarTriggerProps, + ref + ) => { + const { + setActiveIndex, + menuItems, + registerTopLevelItem, + hasFocus + } = useContext(MenubarContext); + const { id, title, first, last } = useContext(SubmenuContext); + const { isOpen, handlers } = useMenuProps(id); + + const handleMouseEnter = () => { + if (hasFocus) { + const items = Array.from(menuItems); + const index = items.findIndex((item) => item === ref.current); + + if (index !== -1) { + setActiveIndex(index); } - break; - case 'Enter': - case ' ': - if (!isOpen) { - e.preventDefault(); - e.stopPropagation(); - first(); - } - break; - default: - break; - } - }; - - useEffect(() => { - const unregister = registerTopLevelItem(ref, id); - return unregister; - }, [menuItems, registerTopLevelItem]); - - return ( - - ); -}); + } + }; -MenubarTrigger.propTypes = { - role: PropTypes.string, - hasPopup: PropTypes.oneOf(['menu', 'listbox', 'true']) -}; + const handleKeyDown = (e: React.KeyboardEvent) => { + switch (e.key) { + case 'ArrowDown': + if (!isOpen) { + e.preventDefault(); + e.stopPropagation(); + first(); + } + break; + case 'ArrowUp': + if (!isOpen) { + e.preventDefault(); + e.stopPropagation(); + last(); + } + break; + case 'Enter': + case ' ': + if (!isOpen) { + e.preventDefault(); + e.stopPropagation(); + first(); + } + break; + default: + break; + } + }; -MenubarTrigger.defaultProps = { - role: 'menuitem', - hasPopup: 'menu' -}; + useEffect(() => { + const unregister = registerTopLevelItem(ref, id); + return unregister; + }, [menuItems, registerTopLevelItem]); + + return ( + + ); + } +); /* ------------------------------------------------------------------------------------------------- * MenubarList * -----------------------------------------------------------------------------------------------*/ +interface MenubarListProps { + // MenubarItems that should be rendered in the list + children?: React.ReactNode; + // The ARIA role of the list element + role?: 'menu' | 'listbox'; +} + /** * MenubarList renders the container for menu items in a submenu. It provides context and handles ARIA roles. * - * @param {Object} props - * @param {React.ReactNode} props.children - MenubarItems that should be rendered in the list - * @param {string} [props.role='menu'] - The ARIA role of the list element - * @returns {JSX.Element} - * * @example * * ... elements * */ - -function MenubarList({ children, role, ...props }) { +function MenubarList({ children, role = 'menu', ...props }: MenubarListProps) { const { id, title } = useContext(SubmenuContext); return ( @@ -184,20 +184,18 @@ function MenubarList({ children, role, ...props }) { ); } -MenubarList.propTypes = { - children: PropTypes.node, - role: PropTypes.oneOf(['menu', 'listbox']) -}; - -MenubarList.defaultProps = { - children: null, - role: 'menu' -}; - /* ------------------------------------------------------------------------------------------------- * MenubarSubmenu * -----------------------------------------------------------------------------------------------*/ +export interface MenubarSubmenuProps { + id: string; + children?: React.ReactNode; + title: string; + triggerRole?: string; + listRole?: 'menu' | 'listbox'; +} + /** * MenubarSubmenu manages a triggerable submenu within a menubar. It is a compound component * that manages the state of the submenu and its items. It also provides keyboard navigation @@ -219,15 +217,14 @@ MenubarList.defaultProps = { * * */ - -function MenubarSubmenu({ +export function MenubarSubmenu({ children, id, title, - triggerRole: customTriggerRole, - listRole: customListRole, + triggerRole: customTriggerRole = 'menuItem', + listRole: customListRole = 'menu', ...props -}) { +}: MenubarSubmenuProps) { const { isOpen, handlers } = useMenuProps(id); const [submenuActiveIndex, setSubmenuActiveIndex] = useState(0); const { setMenuOpen, toggleMenuOpen } = useContext(MenubarContext); @@ -442,19 +439,3 @@ function MenubarSubmenu({ ); } - -MenubarSubmenu.propTypes = { - id: PropTypes.string.isRequired, - children: PropTypes.node, - title: PropTypes.node.isRequired, - triggerRole: PropTypes.string, - listRole: PropTypes.string -}; - -MenubarSubmenu.defaultProps = { - children: null, - triggerRole: 'menuitem', - listRole: 'menu' -}; - -export default MenubarSubmenu; diff --git a/client/components/Menubar/contexts.tsx b/client/components/Menubar/contexts.tsx index 93e5f822fd..21b41184e0 100644 --- a/client/components/Menubar/contexts.tsx +++ b/client/components/Menubar/contexts.tsx @@ -5,27 +5,54 @@ export const ParentMenuContext = createContext('none'); export const MenuOpenContext = createContext('none'); interface MenubarContextType { + // Menubar createMenuHandlers: (id: string) => Record; - createMenuItemHandlers: (id: string) => Record; toggleMenuOpen: (id: string) => void; - setMenuOpen?: (id: string) => void; + setMenuOpen: (id: string) => void; + + // MenubarItem + createMenuItemHandlers: (id: string) => Record; hasFocus?: boolean; + + // MenubarSubmenu + setActiveIndex: (index: number) => void; + menuItems: Set; + registerTopLevelItem: (ref: unknown, id: string) => void; } + export const MenubarContext = createContext({ createMenuHandlers: () => ({}), createMenuItemHandlers: () => ({}), toggleMenuOpen: () => {}, - hasFocus: false + setMenuOpen: () => {}, + setActiveIndex: () => {}, + hasFocus: false, + menuItems: Set, + registerTopLevelItem(ref: unknown, id: string): void { + throw new Error('Function not implemented.'); + } }); interface SubmenuContextType { submenuItems: Set; setSubmenuActiveIndex: (index: number) => void; registerSubmenuItem: (ref: React.RefObject) => () => void; + id: string; + title: string; + first: () => {}; + last: () => {}; } export const SubmenuContext = createContext({ submenuItems: new Set(), setSubmenuActiveIndex: () => {}, - registerSubmenuItem: () => () => {} + registerSubmenuItem: () => () => {}, + id: '', + title: '', + first(): {} { + throw new Error('Function not implemented.'); + }, + last(): {} { + throw new Error('Function not implemented.'); + } }); diff --git a/client/modules/IDE/components/Header/Nav.jsx b/client/modules/IDE/components/Header/Nav.jsx index 25c849aa6d..511b70adc4 100644 --- a/client/modules/IDE/components/Header/Nav.jsx +++ b/client/modules/IDE/components/Header/Nav.jsx @@ -4,7 +4,7 @@ import { sortBy } from 'lodash'; import { Link } from 'react-router-dom'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; -import MenubarSubmenu from '../../../../components/Menubar/MenubarSubmenu'; +import { MenubarSubmenu } from '../../../../components/Menubar/MenubarSubmenu'; import { MenubarItem } from '../../../../components/Menubar/MenubarItem'; import { availableLanguages, languageKeyToLabel } from '../../../../i18n'; import { getConfig } from '../../../../utils/getConfig'; diff --git a/client/modules/IDE/components/Header/Nav.unit.test.jsx b/client/modules/IDE/components/Header/Nav.unit.test.jsx index 289da87e34..d5c2465c0c 100644 --- a/client/modules/IDE/components/Header/Nav.unit.test.jsx +++ b/client/modules/IDE/components/Header/Nav.unit.test.jsx @@ -3,6 +3,7 @@ import React from 'react'; import { reduxRender } from '../../../../test-utils'; import Nav from './Nav'; +import { MenubarSubmenu } from '../../../../components/Menubar/MenubarSubmenu'; jest.mock('../../../../utils/generateRandomName'); @@ -16,25 +17,35 @@ jest.mock('../../../../components/Menubar/Menubar', () => ({ })); // mock MenubarSubmenu -jest.mock('../../../../components/Menubar/MenubarSubmenu', () => { - function MenubarSubmenu({ children, title }) { - return ( -
  • - {title} -
      - {children} -
    -
  • - ); - } +jest.mock('../../../../components/Menubar/MenubarSubmenu', () => ({ + MenubarSubmenu: ({ children, title }) => ( +
  • + {title} +
      + {children} +
    +
  • + ) +})); +// jest.mock('../../../../components/Menubar/MenubarSubmenu', () => { +// function MenubarSubmenu({ children, title }) { +// return ( +//
  • +// {title} +//
      +// {children} +//
    +//
  • +// ); +// } - MenubarSubmenu.useMenuProps = () => ({ - isOpen: false, - handlers: {} - }); +// MenubarSubmenu.useMenuProps = () => ({ +// isOpen: false, +// handlers: {} +// }); - return MenubarSubmenu; -}); +// return MenubarSubmenu; +// }); // mock MenubarItem jest.mock('../../../../components/Menubar/MenubarItem', () => ({ From 11fcf491d1a84d692f7eb2666cfe8d622d51ddf7 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 1 Sep 2025 15:47:12 +0100 Subject: [PATCH 75/76] WIP fix type errors on MenubarSubmenu --no-verify --- client/components/Menubar/Menubar.tsx | 6 ++-- client/components/Menubar/MenubarSubmenu.tsx | 8 +++-- client/components/Menubar/contexts.tsx | 31 +++++++++----------- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/client/components/Menubar/Menubar.tsx b/client/components/Menubar/Menubar.tsx index f7a6a6d0a8..46cacdcdb9 100644 --- a/client/components/Menubar/Menubar.tsx +++ b/client/components/Menubar/Menubar.tsx @@ -12,7 +12,9 @@ import { usePrevious } from '../../common/usePrevious'; import { KeydownHandlerMap } from '../../common/useKeyDownHandlers'; export interface MenubarProps { + // Menu items that will be rendered in the menubar children?: React.ReactNode; + // CSS class name to apply to the menubar className?: string; } @@ -20,10 +22,6 @@ export interface MenubarProps { * Menubar manages a collection of menu items and their submenus. It provides keyboard navigation, * focus and state management, and other accessibility features for the menu items and submenus. * - * @param {React.ReactNode} props.children - Menu items that will be rendered in the menubar - * @param {string} [props.className='nav__menubar'] - CSS class name to apply to the menubar - * @returns {JSX.Element} - * * @example * * diff --git a/client/components/Menubar/MenubarSubmenu.tsx b/client/components/Menubar/MenubarSubmenu.tsx index 4f9f51c032..c1c3f748e3 100644 --- a/client/components/Menubar/MenubarSubmenu.tsx +++ b/client/components/Menubar/MenubarSubmenu.tsx @@ -84,7 +84,11 @@ const MenubarTrigger = React.forwardRef( const handleMouseEnter = () => { if (hasFocus) { const items = Array.from(menuItems); - const index = items.findIndex((item) => item === ref.current); + const index = items.findIndex( + (item) => + item === + (ref as React.MutableRefObject).current + ); if (index !== -1) { setActiveIndex(index); @@ -228,7 +232,7 @@ export function MenubarSubmenu({ const { isOpen, handlers } = useMenuProps(id); const [submenuActiveIndex, setSubmenuActiveIndex] = useState(0); const { setMenuOpen, toggleMenuOpen } = useContext(MenubarContext); - const submenuItems = useRef(new Set()).current; + const submenuItems = useRef>(new Set()).current; const buttonRef = useRef(null); const listItemRef = useRef(null); diff --git a/client/components/Menubar/contexts.tsx b/client/components/Menubar/contexts.tsx index 21b41184e0..5369b1f497 100644 --- a/client/components/Menubar/contexts.tsx +++ b/client/components/Menubar/contexts.tsx @@ -12,12 +12,15 @@ interface MenubarContextType { // MenubarItem createMenuItemHandlers: (id: string) => Record; - hasFocus?: boolean; + hasFocus: boolean; // MenubarSubmenu setActiveIndex: (index: number) => void; - menuItems: Set; - registerTopLevelItem: (ref: unknown, id: string) => void; + menuItems: Set; + registerTopLevelItem: ( + ref: React.Ref, + id: string + ) => () => void; // returns unregister fn } export const MenubarContext = createContext({ @@ -27,20 +30,18 @@ export const MenubarContext = createContext({ setMenuOpen: () => {}, setActiveIndex: () => {}, hasFocus: false, - menuItems: Set, - registerTopLevelItem(ref: unknown, id: string): void { - throw new Error('Function not implemented.'); - } + menuItems: new Set(), + registerTopLevelItem: () => () => {} }); interface SubmenuContextType { - submenuItems: Set; + submenuItems: Set; setSubmenuActiveIndex: (index: number) => void; - registerSubmenuItem: (ref: React.RefObject) => () => void; + registerSubmenuItem: (ref: React.Ref) => () => void; id: string; title: string; - first: () => {}; - last: () => {}; + first: () => void; + last: () => void; } export const SubmenuContext = createContext({ @@ -49,10 +50,6 @@ export const SubmenuContext = createContext({ registerSubmenuItem: () => () => {}, id: '', title: '', - first(): {} { - throw new Error('Function not implemented.'); - }, - last(): {} { - throw new Error('Function not implemented.'); - } + first: () => {}, + last: () => {} }); From de2f4046046f1fd83bbedd7f8a67ea0422c55c23 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 1 Sep 2025 15:54:59 +0100 Subject: [PATCH 76/76] wip -- abandon --no-verify --- client/components/Menubar/MenubarSubmenu.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/components/Menubar/MenubarSubmenu.tsx b/client/components/Menubar/MenubarSubmenu.tsx index c1c3f748e3..ca159742d3 100644 --- a/client/components/Menubar/MenubarSubmenu.tsx +++ b/client/components/Menubar/MenubarSubmenu.tsx @@ -276,6 +276,8 @@ export function MenubarSubmenu({ if (activeItem) { const activeItemNode = activeItem.firstChild; + if (!activeItemNode) return; + const isDisabled = activeItemNode.getAttribute('aria-disabled') === 'true';