Skip to content

Commit 50c2821

Browse files
committed
MenubarSubmenu: wip - update context related type errors, and update menuBarSubMenu related useages -- no-verify
1 parent 9b1e574 commit 50c2821

File tree

6 files changed

+168
-149
lines changed

6 files changed

+168
-149
lines changed

client/components/Dropdown/DropdownMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export const DropdownMenu = forwardRef<HTMLDivElement, DropdownMenuProps>(
108108

109109
const close = useCallback(() => setIsOpen(false), [setIsOpen]);
110110

111-
const anchorRef = useModalClose(close, ref);
111+
const anchorRef = useModalClose<HTMLDivElement>(close, ref);
112112

113113
const toggle = useCallback(() => {
114114
setIsOpen((prevState) => !prevState);

client/components/Menubar/Menubar.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import { render, screen, fireEvent } from '../../test-utils';
33
import { Menubar } from './Menubar';
4-
import MenubarSubmenu from './MenubarSubmenu';
4+
import { MenubarSubmenu } from './MenubarSubmenu';
55
import { MenubarItem } from './MenubarItem';
66

77
describe('Menubar', () => {

client/components/Menubar/MenubarSubmenu.tsx

Lines changed: 106 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// https://blog.logrocket.com/building-accessible-menubar-component-react
22

33
import classNames from 'classnames';
4-
import PropTypes from 'prop-types';
54
import React, {
65
useState,
76
useEffect,
@@ -18,11 +17,12 @@ import {
1817
} from './contexts';
1918
import TriangleIcon from '../../images/down-filled-triangle.svg';
2019

21-
export function useMenuProps(id) {
20+
/* -------------------------------------------------------------------------------------------------
21+
* useMenuProps
22+
* -----------------------------------------------------------------------------------------------*/
23+
export function useMenuProps(id: string) {
2224
const activeMenu = useContext(MenuOpenContext);
23-
2425
const isOpen = id === activeMenu;
25-
2626
const { createMenuHandlers } = useContext(MenubarContext);
2727

2828
const handlers = useMemo(() => createMenuHandlers(id), [
@@ -36,7 +36,11 @@ export function useMenuProps(id) {
3636
/* -------------------------------------------------------------------------------------------------
3737
* MenubarTrigger
3838
* -----------------------------------------------------------------------------------------------*/
39-
39+
interface MenubarTriggerProps
40+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
41+
role?: string;
42+
hasPopup?: 'menu' | 'listbox' | 'true';
43+
}
4044
/**
4145
* MenubarTrigger renders a button that toggles a submenu. It handles keyboard navigation and supports
4246
* screen readers. It needs to be within a submenu context.
@@ -63,111 +67,107 @@ export function useMenuProps(id) {
6367
* </li>
6468
*/
6569

66-
const MenubarTrigger = React.forwardRef(({ role, hasPopup, ...props }, ref) => {
67-
const {
68-
setActiveIndex,
69-
menuItems,
70-
registerTopLevelItem,
71-
hasFocus
72-
} = useContext(MenubarContext);
73-
const { id, title, first, last } = useContext(SubmenuContext);
74-
const { isOpen, handlers } = useMenuProps(id);
75-
76-
const handleMouseEnter = () => {
77-
if (hasFocus) {
78-
const items = Array.from(menuItems);
79-
const index = items.findIndex((item) => item === ref.current);
80-
81-
if (index !== -1) {
82-
setActiveIndex(index);
83-
}
84-
}
85-
};
86-
87-
const handleKeyDown = (e) => {
88-
switch (e.key) {
89-
case 'ArrowDown':
90-
if (!isOpen) {
91-
e.preventDefault();
92-
e.stopPropagation();
93-
first();
94-
}
95-
break;
96-
case 'ArrowUp':
97-
if (!isOpen) {
98-
e.preventDefault();
99-
e.stopPropagation();
100-
last();
70+
const MenubarTrigger = React.forwardRef<HTMLButtonElement, MenubarTriggerProps>(
71+
(
72+
{ role = 'menuitem', hasPopup = 'menu', ...props }: MenubarTriggerProps,
73+
ref
74+
) => {
75+
const {
76+
setActiveIndex,
77+
menuItems,
78+
registerTopLevelItem,
79+
hasFocus
80+
} = useContext(MenubarContext);
81+
const { id, title, first, last } = useContext(SubmenuContext);
82+
const { isOpen, handlers } = useMenuProps(id);
83+
84+
const handleMouseEnter = () => {
85+
if (hasFocus) {
86+
const items = Array.from(menuItems);
87+
const index = items.findIndex((item) => item === ref.current);
88+
89+
if (index !== -1) {
90+
setActiveIndex(index);
10191
}
102-
break;
103-
case 'Enter':
104-
case ' ':
105-
if (!isOpen) {
106-
e.preventDefault();
107-
e.stopPropagation();
108-
first();
109-
}
110-
break;
111-
default:
112-
break;
113-
}
114-
};
115-
116-
useEffect(() => {
117-
const unregister = registerTopLevelItem(ref, id);
118-
return unregister;
119-
}, [menuItems, registerTopLevelItem]);
120-
121-
return (
122-
<button
123-
{...props}
124-
{...handlers}
125-
ref={ref}
126-
role={role}
127-
onMouseEnter={handleMouseEnter}
128-
onKeyDown={handleKeyDown}
129-
aria-haspopup={hasPopup}
130-
aria-expanded={isOpen}
131-
>
132-
<span className="nav__item-header">{title}</span>
133-
<TriangleIcon
134-
className="nav__item-header-triangle"
135-
focusable="false"
136-
aria-hidden="true"
137-
/>
138-
</button>
139-
);
140-
});
92+
}
93+
};
14194

142-
MenubarTrigger.propTypes = {
143-
role: PropTypes.string,
144-
hasPopup: PropTypes.oneOf(['menu', 'listbox', 'true'])
145-
};
95+
const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
96+
switch (e.key) {
97+
case 'ArrowDown':
98+
if (!isOpen) {
99+
e.preventDefault();
100+
e.stopPropagation();
101+
first();
102+
}
103+
break;
104+
case 'ArrowUp':
105+
if (!isOpen) {
106+
e.preventDefault();
107+
e.stopPropagation();
108+
last();
109+
}
110+
break;
111+
case 'Enter':
112+
case ' ':
113+
if (!isOpen) {
114+
e.preventDefault();
115+
e.stopPropagation();
116+
first();
117+
}
118+
break;
119+
default:
120+
break;
121+
}
122+
};
146123

147-
MenubarTrigger.defaultProps = {
148-
role: 'menuitem',
149-
hasPopup: 'menu'
150-
};
124+
useEffect(() => {
125+
const unregister = registerTopLevelItem(ref, id);
126+
return unregister;
127+
}, [menuItems, registerTopLevelItem]);
128+
129+
return (
130+
<button
131+
{...props}
132+
{...handlers}
133+
ref={ref}
134+
role={role}
135+
onMouseEnter={handleMouseEnter}
136+
onKeyDown={handleKeyDown}
137+
aria-haspopup={hasPopup}
138+
aria-expanded={isOpen}
139+
>
140+
<span className="nav__item-header">{title}</span>
141+
<TriangleIcon
142+
className="nav__item-header-triangle"
143+
focusable="false"
144+
aria-hidden="true"
145+
/>
146+
</button>
147+
);
148+
}
149+
);
151150

152151
/* -------------------------------------------------------------------------------------------------
153152
* MenubarList
154153
* -----------------------------------------------------------------------------------------------*/
155154

155+
interface MenubarListProps {
156+
// MenubarItems that should be rendered in the list
157+
children?: React.ReactNode;
158+
// The ARIA role of the list element
159+
role?: 'menu' | 'listbox';
160+
}
161+
156162
/**
157163
* MenubarList renders the container for menu items in a submenu. It provides context and handles ARIA roles.
158164
*
159-
* @param {Object} props
160-
* @param {React.ReactNode} props.children - MenubarItems that should be rendered in the list
161-
* @param {string} [props.role='menu'] - The ARIA role of the list element
162-
* @returns {JSX.Element}
163-
*
164165
* @example
165166
* <MenubarList role={listRole}>
166167
* ... <MenubarItem> elements
167168
* </MenubarList>
168169
*/
169-
170-
function MenubarList({ children, role, ...props }) {
170+
function MenubarList({ children, role = 'menu', ...props }: MenubarListProps) {
171171
const { id, title } = useContext(SubmenuContext);
172172

173173
return (
@@ -184,20 +184,18 @@ function MenubarList({ children, role, ...props }) {
184184
);
185185
}
186186

187-
MenubarList.propTypes = {
188-
children: PropTypes.node,
189-
role: PropTypes.oneOf(['menu', 'listbox'])
190-
};
191-
192-
MenubarList.defaultProps = {
193-
children: null,
194-
role: 'menu'
195-
};
196-
197187
/* -------------------------------------------------------------------------------------------------
198188
* MenubarSubmenu
199189
* -----------------------------------------------------------------------------------------------*/
200190

191+
export interface MenubarSubmenuProps {
192+
id: string;
193+
children?: React.ReactNode;
194+
title: string;
195+
triggerRole?: string;
196+
listRole?: 'menu' | 'listbox';
197+
}
198+
201199
/**
202200
* MenubarSubmenu manages a triggerable submenu within a menubar. It is a compound component
203201
* that manages the state of the submenu and its items. It also provides keyboard navigation
@@ -219,15 +217,14 @@ MenubarList.defaultProps = {
219217
* </MenubarSubmenu>
220218
* </Menubar>
221219
*/
222-
223-
function MenubarSubmenu({
220+
export function MenubarSubmenu({
224221
children,
225222
id,
226223
title,
227-
triggerRole: customTriggerRole,
228-
listRole: customListRole,
224+
triggerRole: customTriggerRole = 'menuItem',
225+
listRole: customListRole = 'menu',
229226
...props
230-
}) {
227+
}: MenubarSubmenuProps) {
231228
const { isOpen, handlers } = useMenuProps(id);
232229
const [submenuActiveIndex, setSubmenuActiveIndex] = useState(0);
233230
const { setMenuOpen, toggleMenuOpen } = useContext(MenubarContext);
@@ -442,19 +439,3 @@ function MenubarSubmenu({
442439
</SubmenuContext.Provider>
443440
);
444441
}
445-
446-
MenubarSubmenu.propTypes = {
447-
id: PropTypes.string.isRequired,
448-
children: PropTypes.node,
449-
title: PropTypes.node.isRequired,
450-
triggerRole: PropTypes.string,
451-
listRole: PropTypes.string
452-
};
453-
454-
MenubarSubmenu.defaultProps = {
455-
children: null,
456-
triggerRole: 'menuitem',
457-
listRole: 'menu'
458-
};
459-
460-
export default MenubarSubmenu;

client/components/Menubar/contexts.tsx

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,54 @@ export const ParentMenuContext = createContext<string>('none');
55
export const MenuOpenContext = createContext<string>('none');
66

77
interface MenubarContextType {
8+
// Menubar
89
createMenuHandlers: (id: string) => Record<string, any>;
9-
createMenuItemHandlers: (id: string) => Record<string, any>;
1010
toggleMenuOpen: (id: string) => void;
11-
setMenuOpen?: (id: string) => void;
11+
setMenuOpen: (id: string) => void;
12+
13+
// MenubarItem
14+
createMenuItemHandlers: (id: string) => Record<string, any>;
1215
hasFocus?: boolean;
16+
17+
// MenubarSubmenu
18+
setActiveIndex: (index: number) => void;
19+
menuItems: Set<HTMLElement>;
20+
registerTopLevelItem: (ref: unknown, id: string) => void;
1321
}
22+
1423
export const MenubarContext = createContext<MenubarContextType>({
1524
createMenuHandlers: () => ({}),
1625
createMenuItemHandlers: () => ({}),
1726
toggleMenuOpen: () => {},
18-
hasFocus: false
27+
setMenuOpen: () => {},
28+
setActiveIndex: () => {},
29+
hasFocus: false,
30+
menuItems: Set<HTMLElement>,
31+
registerTopLevelItem(ref: unknown, id: string): void {
32+
throw new Error('Function not implemented.');
33+
}
1934
});
2035

2136
interface SubmenuContextType {
2237
submenuItems: Set<HTMLElement>;
2338
setSubmenuActiveIndex: (index: number) => void;
2439
registerSubmenuItem: (ref: React.RefObject<HTMLElement>) => () => void;
40+
id: string;
41+
title: string;
42+
first: () => {};
43+
last: () => {};
2544
}
2645

2746
export const SubmenuContext = createContext<SubmenuContextType>({
2847
submenuItems: new Set(),
2948
setSubmenuActiveIndex: () => {},
30-
registerSubmenuItem: () => () => {}
49+
registerSubmenuItem: () => () => {},
50+
id: '',
51+
title: '',
52+
first(): {} {
53+
throw new Error('Function not implemented.');
54+
},
55+
last(): {} {
56+
throw new Error('Function not implemented.');
57+
}
3158
});

client/modules/IDE/components/Header/Nav.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { sortBy } from 'lodash';
44
import { Link } from 'react-router-dom';
55
import PropTypes from 'prop-types';
66
import { useTranslation } from 'react-i18next';
7-
import MenubarSubmenu from '../../../../components/Menubar/MenubarSubmenu';
7+
import { MenubarSubmenu } from '../../../../components/Menubar/MenubarSubmenu';
88
import { MenubarItem } from '../../../../components/Menubar/MenubarItem';
99
import { availableLanguages, languageKeyToLabel } from '../../../../i18n';
1010
import { getConfig } from '../../../../utils/getConfig';

0 commit comments

Comments
 (0)