Skip to content

Commit ca99e21

Browse files
committed
Menubar: add interface, fix type errors, update context types and update to named export
1 parent 5a74f2a commit ca99e21

File tree

6 files changed

+72
-52
lines changed

6 files changed

+72
-52
lines changed

client/components/Menubar/Menubar.test.tsx

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

client/components/Menubar/Menubar.tsx

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import PropTypes from 'prop-types';
21
import React, {
32
useCallback,
43
useMemo,
@@ -14,10 +13,6 @@ import { usePrevious } from '../../common/usePrevious';
1413
* Menubar manages a collection of menu items and their submenus. It provides keyboard navigation,
1514
* focus and state management, and other accessibility features for the menu items and submenus.
1615
*
17-
* @param {React.ReactNode} props.children - Menu items that will be rendered in the menubar
18-
* @param {string} [props.className='nav__menubar'] - CSS class name to apply to the menubar
19-
* @returns {JSX.Element}
20-
*
2116
* @example
2217
* <Menubar>
2318
* <MenubarSubmenu id="file" title="File">
@@ -26,16 +21,26 @@ import { usePrevious } from '../../common/usePrevious';
2621
* </Menubar>
2722
*/
2823

29-
function Menubar({ children, className }) {
30-
const [menuOpen, setMenuOpen] = useState('none');
31-
const [activeIndex, setActiveIndex] = useState(0);
32-
const prevIndex = usePrevious(activeIndex);
33-
const [hasFocus, setHasFocus] = useState(false);
24+
export interface MenubarProps {
25+
/** Menu items that will be rendered in the menubar */
26+
children?: React.ReactNode;
27+
/** CSS class name to apply to the menubar */
28+
className?: string;
29+
}
30+
31+
export function Menubar({
32+
children,
33+
className = 'nav__menubar'
34+
}: MenubarProps) {
35+
const [menuOpen, setMenuOpen] = useState<string>('none');
36+
const [activeIndex, setActiveIndex] = useState<number>(0);
37+
const prevIndex = usePrevious<number>(activeIndex);
38+
const [hasFocus, setHasFocus] = useState<boolean>(false);
3439

35-
const menuItems = useRef(new Set()).current;
40+
const menuItems = useRef<Set<HTMLUListElement>>(new Set()).current;
3641
const menuItemToId = useRef(new Map()).current;
3742

38-
const timerRef = useRef(null);
43+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
3944

4045
const getMenuId = useCallback(
4146
(index) => {
@@ -85,7 +90,7 @@ function Menubar({ children, className }) {
8590

8691
const toggleMenuOpen = useCallback((id) => {
8792
setMenuOpen((prevState) => (prevState === id ? 'none' : id));
88-
});
93+
}, []);
8994

9095
const registerTopLevelItem = useCallback(
9196
(ref, submenuId) => {
@@ -105,7 +110,7 @@ function Menubar({ children, className }) {
105110
);
106111

107112
const clearHideTimeout = useCallback(() => {
108-
if (timerRef.current) {
113+
if (timerRef.current !== null) {
109114
clearTimeout(timerRef.current);
110115
timerRef.current = null;
111116
}
@@ -116,7 +121,7 @@ function Menubar({ children, className }) {
116121
setMenuOpen('none');
117122
}, [setMenuOpen]);
118123

119-
const nodeRef = useModalClose(handleClose);
124+
const nodeRef = useModalClose<HTMLUListElement>(handleClose);
120125

121126
const handleFocus = useCallback(() => {
122127
setHasFocus(true);
@@ -138,7 +143,7 @@ function Menubar({ children, className }) {
138143
[nodeRef]
139144
);
140145

141-
const keyHandlers = {
146+
const keyHandlers: Record<string, (e: React.KeyboardEvent) => void> = {
142147
ArrowLeft: (e) => {
143148
e.preventDefault();
144149
e.stopPropagation();
@@ -173,8 +178,11 @@ function Menubar({ children, className }) {
173178
useEffect(() => {
174179
if (activeIndex !== prevIndex) {
175180
const items = Array.from(menuItems);
181+
const prevNode =
182+
prevIndex != null /** check against undefined or null */
183+
? items[prevIndex]
184+
: undefined;
176185
const activeNode = items[activeIndex];
177-
const prevNode = items[prevIndex];
178186

179187
prevNode?.setAttribute('tabindex', '-1');
180188
activeNode?.setAttribute('tabindex', '0');
@@ -191,7 +199,7 @@ function Menubar({ children, className }) {
191199

192200
const contextValue = useMemo(
193201
() => ({
194-
createMenuHandlers: (menu) => ({
202+
createMenuHandlers: (menu: string) => ({
195203
onMouseOver: () => {
196204
setMenuOpen((prevState) => (prevState === 'none' ? 'none' : menu));
197205
},
@@ -210,8 +218,8 @@ function Menubar({ children, className }) {
210218
onBlur: handleBlur,
211219
onFocus: clearHideTimeout
212220
}),
213-
createMenuItemHandlers: (menu) => ({
214-
onMouseUp: (e) => {
221+
createMenuItemHandlers: (menu: string) => ({
222+
onMouseUp: (e: React.MouseEvent) => {
215223
if (e.button === 2) {
216224
return;
217225
}
@@ -278,15 +286,3 @@ function Menubar({ children, className }) {
278286
</MenubarContext.Provider>
279287
);
280288
}
281-
282-
Menubar.propTypes = {
283-
children: PropTypes.node,
284-
className: PropTypes.string
285-
};
286-
287-
Menubar.defaultProps = {
288-
children: null,
289-
className: 'nav__menubar'
290-
};
291-
292-
export default Menubar;

client/components/Menubar/contexts.tsx

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,40 @@
1-
import { createContext } from 'react';
1+
import React, { createContext } from 'react';
22

3-
export const ParentMenuContext = createContext('none');
3+
export const ParentMenuContext = createContext<string>('none');
44

5-
export const MenuOpenContext = createContext('none');
5+
export const MenuOpenContext = createContext<string>('none');
66

7-
export const MenubarContext = createContext({
8-
createMenuHandlers: () => ({}),
9-
createMenuItemHandlers: () => ({}),
7+
interface MenubarContextType {
8+
createMenuHandlers: (
9+
id: string
10+
) => {
11+
onMouseOver: (e: React.MouseEvent) => void;
12+
onClick: (e: React.MouseEvent) => void;
13+
onBlur: (e: React.FocusEvent) => void;
14+
onFocus: (e: React.FocusEvent) => void;
15+
};
16+
createMenuItemHandlers: (
17+
id: string
18+
) => {
19+
onMouseUp: (e: React.MouseEvent) => void;
20+
onBlur: (e: React.FocusEvent) => void;
21+
onFocus: (e: React.FocusEvent) => void;
22+
};
23+
toggleMenuOpen: (id: string) => void;
24+
}
25+
26+
export const MenubarContext = createContext<MenubarContextType>({
27+
createMenuHandlers: () => ({
28+
onMouseOver: () => {},
29+
onClick: () => {},
30+
onBlur: () => {},
31+
onFocus: () => {}
32+
}),
33+
createMenuItemHandlers: () => ({
34+
onMouseUp: () => {},
35+
onBlur: () => {},
36+
onFocus: () => {}
37+
}),
1038
toggleMenuOpen: () => {}
1139
});
1240

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Link } from 'react-router-dom';
66
import { sortBy } from 'lodash';
77
import classNames from 'classnames';
88
import { ParentMenuContext } from '../../../../components/Menubar/contexts';
9-
import Menubar from '../../../../components/Menubar/Menubar';
9+
import { Menubar } from '../../../../components/Menubar/Menubar';
1010
import { useMenuProps } from '../../../../components/Menubar/MenubarSubmenu';
1111
import { ButtonOrLink } from '../../../../common/ButtonOrLink';
1212
import { prop, remSize } from '../../../../theme';

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { getConfig } from '../../../../utils/getConfig';
1111
import { parseBoolean } from '../../../../utils/parseStringToType';
1212
import { showToast } from '../../actions/toast';
1313
import { setLanguage } from '../../actions/preferences';
14-
import Menubar from '../../../../components/Menubar/Menubar';
14+
import { Menubar } from '../../../../components/Menubar/Menubar';
1515
import CaretLeftIcon from '../../../../images/left-arrow.svg';
1616
import LogoIcon from '../../../../images/p5js-logo-small.svg';
1717
import { selectRootFile } from '../../selectors/files';

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

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,13 @@ import Nav from './Nav';
77
jest.mock('../../../../utils/generateRandomName');
88

99
// mock Menubar
10-
jest.mock(
11-
'../../../../components/Menubar/Menubar',
12-
() =>
13-
function Menubar({ children, className = 'nav__menubar' }) {
14-
return (
15-
<ul className={className} role="menubar">
16-
{children}
17-
</ul>
18-
);
19-
}
20-
);
10+
jest.mock('../../../../components/Menubar/Menubar', () => ({
11+
Menubar: ({ children, className = 'nav__menubar' }) => (
12+
<ul className={className} role="menubar">
13+
{children}
14+
</ul>
15+
)
16+
}));
2117

2218
// mock MenubarSubmenu
2319
jest.mock('../../../../components/Menubar/MenubarSubmenu', () => {

0 commit comments

Comments
 (0)