Skip to content

Commit a4ddf9b

Browse files
authored
Merge pull request #3279 from tespin/tespin/refactor-navbar-base
[Menubar] Base implementation of refactored NavBar
2 parents 9bd7c99 + de201ff commit a4ddf9b

File tree

11 files changed

+754
-647
lines changed

11 files changed

+754
-647
lines changed

client/components/Menubar/Menubar.jsx

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import PropTypes from 'prop-types';
2+
import React, { useCallback, useMemo, useRef, useState } from 'react';
3+
import useModalClose from '../../common/useModalClose';
4+
import { MenuOpenContext, MenubarContext } from './contexts';
5+
6+
function Menubar({ children, className }) {
7+
const [menuOpen, setMenuOpen] = useState('none');
8+
9+
const timerRef = useRef(null);
10+
11+
const handleClose = useCallback(() => {
12+
setMenuOpen('none');
13+
}, [setMenuOpen]);
14+
15+
const nodeRef = useModalClose(handleClose);
16+
17+
const clearHideTimeout = useCallback(() => {
18+
if (timerRef.current) {
19+
clearTimeout(timerRef.current);
20+
timerRef.current = null;
21+
}
22+
}, [timerRef]);
23+
24+
const handleBlur = useCallback(() => {
25+
timerRef.current = setTimeout(() => setMenuOpen('none'), 10);
26+
}, [timerRef, setMenuOpen]);
27+
28+
const toggleMenuOpen = useCallback(
29+
(menu) => {
30+
setMenuOpen((prevState) => (prevState === menu ? 'none' : menu));
31+
},
32+
[setMenuOpen]
33+
);
34+
35+
const contextValue = useMemo(
36+
() => ({
37+
createMenuHandlers: (menu) => ({
38+
onMouseOver: () => {
39+
setMenuOpen((prevState) => (prevState === 'none' ? 'none' : menu));
40+
},
41+
onClick: () => {
42+
toggleMenuOpen(menu);
43+
},
44+
onBlur: handleBlur,
45+
onFocus: clearHideTimeout
46+
}),
47+
createMenuItemHandlers: (menu) => ({
48+
onMouseUp: (e) => {
49+
if (e.button === 2) {
50+
return;
51+
}
52+
setMenuOpen('none');
53+
},
54+
onBlur: handleBlur,
55+
onFocus: () => {
56+
clearHideTimeout();
57+
setMenuOpen(menu);
58+
}
59+
}),
60+
toggleMenuOpen
61+
}),
62+
[setMenuOpen, toggleMenuOpen, clearHideTimeout, handleBlur]
63+
);
64+
65+
return (
66+
<MenubarContext.Provider value={contextValue}>
67+
<div className={className} ref={nodeRef}>
68+
<MenuOpenContext.Provider value={menuOpen}>
69+
{children}
70+
</MenuOpenContext.Provider>
71+
</div>
72+
</MenubarContext.Provider>
73+
);
74+
}
75+
76+
Menubar.propTypes = {
77+
children: PropTypes.node,
78+
className: PropTypes.string
79+
};
80+
81+
Menubar.defaultProps = {
82+
children: null,
83+
className: 'nav'
84+
};
85+
86+
export default Menubar;
+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import PropTypes from 'prop-types';
2+
import React, { useContext, useMemo } from 'react';
3+
import ButtonOrLink from '../../common/ButtonOrLink';
4+
import { MenubarContext, ParentMenuContext } from './contexts';
5+
6+
function MenubarItem({
7+
hideIf,
8+
className,
9+
role: customRole,
10+
selected,
11+
...rest
12+
}) {
13+
const parent = useContext(ParentMenuContext);
14+
15+
const { createMenuItemHandlers } = useContext(MenubarContext);
16+
17+
const handlers = useMemo(() => createMenuItemHandlers(parent), [
18+
createMenuItemHandlers,
19+
parent
20+
]);
21+
22+
if (hideIf) {
23+
return null;
24+
}
25+
26+
const role = customRole || 'menuitem';
27+
const ariaSelected = role === 'option' ? { 'aria-selected': selected } : {};
28+
29+
return (
30+
<li className={className}>
31+
<ButtonOrLink {...rest} {...handlers} {...ariaSelected} role={role} />
32+
</li>
33+
);
34+
}
35+
36+
MenubarItem.propTypes = {
37+
...ButtonOrLink.propTypes,
38+
onClick: PropTypes.func,
39+
value: PropTypes.string,
40+
/**
41+
* Provides a way to deal with optional items.
42+
*/
43+
hideIf: PropTypes.bool,
44+
className: PropTypes.string,
45+
role: PropTypes.oneOf(['menuitem', 'option']),
46+
selected: PropTypes.bool
47+
};
48+
49+
MenubarItem.defaultProps = {
50+
onClick: null,
51+
value: null,
52+
hideIf: false,
53+
className: 'nav__dropdown-item',
54+
role: 'menuitem',
55+
selected: false
56+
};
57+
58+
export default MenubarItem;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// https://blog.logrocket.com/building-accessible-menubar-component-react
2+
3+
import classNames from 'classnames';
4+
import PropTypes from 'prop-types';
5+
import React, { useContext, useMemo } from 'react';
6+
import TriangleIcon from '../../images/down-filled-triangle.svg';
7+
import { MenuOpenContext, MenubarContext, ParentMenuContext } from './contexts';
8+
9+
export function useMenuProps(id) {
10+
const activeMenu = useContext(MenuOpenContext);
11+
12+
const isOpen = id === activeMenu;
13+
14+
const { createMenuHandlers } = useContext(MenubarContext);
15+
16+
const handlers = useMemo(() => createMenuHandlers(id), [
17+
createMenuHandlers,
18+
id
19+
]);
20+
21+
return { isOpen, handlers };
22+
}
23+
24+
/* -------------------------------------------------------------------------------------------------
25+
* MenubarTrigger
26+
* -----------------------------------------------------------------------------------------------*/
27+
28+
function MenubarTrigger({ id, title, role, hasPopup, ...props }) {
29+
const { isOpen, handlers } = useMenuProps(id);
30+
31+
return (
32+
<button
33+
{...handlers}
34+
{...props}
35+
role={role}
36+
aria-haspopup={hasPopup}
37+
aria-expanded={isOpen}
38+
>
39+
<span className="nav__item-header">{title}</span>
40+
<TriangleIcon
41+
className="nav__item-header-triangle"
42+
focusable="false"
43+
aria-hidden="true"
44+
/>
45+
</button>
46+
);
47+
}
48+
49+
MenubarTrigger.propTypes = {
50+
id: PropTypes.string.isRequired,
51+
title: PropTypes.node.isRequired,
52+
role: PropTypes.string,
53+
hasPopup: PropTypes.oneOf(['menu', 'listbox', 'true'])
54+
};
55+
56+
MenubarTrigger.defaultProps = {
57+
role: 'menuitem',
58+
hasPopup: 'menu'
59+
};
60+
61+
/* -------------------------------------------------------------------------------------------------
62+
* MenubarList
63+
* -----------------------------------------------------------------------------------------------*/
64+
65+
function MenubarList({ id, children, role, ...props }) {
66+
return (
67+
<ul className="nav__dropdown" role={role} {...props}>
68+
<ParentMenuContext.Provider value={id}>
69+
{children}
70+
</ParentMenuContext.Provider>
71+
</ul>
72+
);
73+
}
74+
75+
MenubarList.propTypes = {
76+
id: PropTypes.string.isRequired,
77+
children: PropTypes.node,
78+
role: PropTypes.oneOf(['menu', 'listbox'])
79+
};
80+
81+
MenubarList.defaultProps = {
82+
children: null,
83+
role: 'menu'
84+
};
85+
86+
/* -------------------------------------------------------------------------------------------------
87+
* MenubarSubmenu
88+
* -----------------------------------------------------------------------------------------------*/
89+
90+
function MenubarSubmenu({
91+
id,
92+
title,
93+
children,
94+
triggerRole: customTriggerRole,
95+
listRole: customListRole,
96+
...props
97+
}) {
98+
const { isOpen } = useMenuProps(id);
99+
100+
const triggerRole = customTriggerRole || 'menuitem';
101+
const listRole = customListRole || 'menu';
102+
103+
const hasPopup = listRole === 'listbox' ? 'listbox' : 'menu';
104+
105+
return (
106+
<li className={classNames('nav__item', isOpen && 'nav__item--open')}>
107+
<MenubarTrigger
108+
id={id}
109+
title={title}
110+
role={triggerRole}
111+
hasPopup={hasPopup}
112+
{...props}
113+
/>
114+
<MenubarList id={id} role={listRole}>
115+
{children}
116+
</MenubarList>
117+
</li>
118+
);
119+
}
120+
121+
MenubarSubmenu.propTypes = {
122+
id: PropTypes.string.isRequired,
123+
title: PropTypes.node.isRequired,
124+
children: PropTypes.node,
125+
triggerRole: PropTypes.string,
126+
listRole: PropTypes.string
127+
};
128+
129+
MenubarSubmenu.defaultProps = {
130+
children: null,
131+
triggerRole: 'menuitem',
132+
listRole: 'menu'
133+
};
134+
135+
export default MenubarSubmenu;

client/components/Nav/contexts.jsx client/components/Menubar/contexts.jsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ export const ParentMenuContext = createContext('none');
44

55
export const MenuOpenContext = createContext('none');
66

7-
export const NavBarContext = createContext({
8-
createDropdownHandlers: () => ({}),
7+
export const MenubarContext = createContext({
8+
createMenuHandlers: () => ({}),
99
createMenuItemHandlers: () => ({}),
10-
toggleDropdownOpen: () => {}
10+
toggleMenuOpen: () => {}
1111
});

client/components/Nav/NavBar.jsx

-92
This file was deleted.

0 commit comments

Comments
 (0)