Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Menubar] Base implementation of refactored NavBar #3279

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
86 changes: 86 additions & 0 deletions client/components/Menubar/Menubar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import PropTypes from 'prop-types';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import useModalClose from '../../common/useModalClose';
import { MenuOpenContext, MenubarContext } from './contexts';

function Menubar({ children, className }) {
const [menuOpen, setMenuOpen] = useState('none');

const timerRef = useRef(null);

const handleClose = useCallback(() => {
setMenuOpen('none');
}, [setMenuOpen]);

const nodeRef = useModalClose(handleClose);

const clearHideTimeout = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
}, [timerRef]);

const handleBlur = useCallback(() => {
timerRef.current = setTimeout(() => setMenuOpen('none'), 10);
}, [timerRef, setMenuOpen]);

const toggleMenuOpen = useCallback(
(menu) => {
setMenuOpen((prevState) => (prevState === menu ? 'none' : menu));
},
[setMenuOpen]
);

const contextValue = useMemo(
() => ({
createMenuHandlers: (menu) => ({
onMouseOver: () => {
setMenuOpen((prevState) => (prevState === 'none' ? 'none' : menu));
},
onClick: () => {
toggleMenuOpen(menu);
},
onBlur: handleBlur,
onFocus: clearHideTimeout
}),
createMenuItemHandlers: (menu) => ({
onMouseUp: (e) => {
if (e.button === 2) {
return;
}
setMenuOpen('none');
},
onBlur: handleBlur,
onFocus: () => {
clearHideTimeout();
setMenuOpen(menu);
}
}),
toggleMenuOpen
}),
[setMenuOpen, toggleMenuOpen, clearHideTimeout, handleBlur]
);

return (
<MenubarContext.Provider value={contextValue}>
<div className={className} ref={nodeRef}>
<MenuOpenContext.Provider value={menuOpen}>
{children}
</MenuOpenContext.Provider>
</div>
</MenubarContext.Provider>
);
}

Menubar.propTypes = {
children: PropTypes.node,
className: PropTypes.string
};

Menubar.defaultProps = {
children: null,
className: 'nav'
};

export default Menubar;
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import PropTypes from 'prop-types';
import React, { useContext, useMemo } from 'react';
import ButtonOrLink from '../../common/ButtonOrLink';
import { NavBarContext, ParentMenuContext } from './contexts';
import { MenubarContext, ParentMenuContext } from './contexts';

function NavMenuItem({ hideIf, className, ...rest }) {
function MenubarItem({ hideIf, className, ...rest }) {
const parent = useContext(ParentMenuContext);

const { createMenuItemHandlers } = useContext(NavBarContext);
const { createMenuItemHandlers } = useContext(MenubarContext);

const handlers = useMemo(() => createMenuItemHandlers(parent), [
createMenuItemHandlers,
Expand All @@ -24,7 +24,7 @@ function NavMenuItem({ hideIf, className, ...rest }) {
);
}

NavMenuItem.propTypes = {
MenubarItem.propTypes = {
...ButtonOrLink.propTypes,
onClick: PropTypes.func,
value: PropTypes.string,
Expand All @@ -35,11 +35,11 @@ NavMenuItem.propTypes = {
className: PropTypes.string
};

NavMenuItem.defaultProps = {
MenubarItem.defaultProps = {
onClick: null,
value: null,
hideIf: false,
className: 'nav__dropdown-item'
};

export default NavMenuItem;
export default MenubarItem;
100 changes: 100 additions & 0 deletions client/components/Menubar/MenubarMenu.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { useContext, useMemo } from 'react';
import TriangleIcon from '../../images/down-filled-triangle.svg';
import { MenuOpenContext, MenubarContext, ParentMenuContext } from './contexts';

export function useMenuProps(id) {
const activeMenu = useContext(MenuOpenContext);

const isOpen = id === activeMenu;

const { createMenuHandlers } = useContext(MenubarContext);

const handlers = useMemo(() => createMenuHandlers(id), [
createMenuHandlers,
id
]);

return { isOpen, handlers };
}

/* -------------------------------------------------------------------------------------------------
* MenubarTrigger
* -----------------------------------------------------------------------------------------------*/

function MenubarTrigger({ id, title, ...props }) {
const { isOpen, handlers } = useMenuProps(id);

return (
<button
{...handlers}
{...props}
role="menuitem"
aria-haspopup="menu"
aria-expanded={isOpen}
>
<span className="nav__item-header">{title}</span>
<TriangleIcon
className="nav__item-header-triangle"
focusable="false"
aria-hidden="true"
/>
</button>
);
}

MenubarTrigger.propTypes = {
id: PropTypes.string.isRequired,
title: PropTypes.node.isRequired
};

/* -------------------------------------------------------------------------------------------------
* MenubarList
* -----------------------------------------------------------------------------------------------*/

function MenubarList({ id, children }) {
return (
<ul className="nav__dropdown" role="menu">
<ParentMenuContext.Provider value={id}>
{children}
</ParentMenuContext.Provider>
</ul>
);
}

MenubarList.propTypes = {
id: PropTypes.string.isRequired,
children: PropTypes.node
};

MenubarList.defaultProps = {
children: null
};

/* -------------------------------------------------------------------------------------------------
* MenubarMenu
* -----------------------------------------------------------------------------------------------*/

function MenubarMenu({ id, title, children }) {
const { isOpen } = useMenuProps(id);

return (
<li className={classNames('nav__item', isOpen && 'nav__item--open')}>
<MenubarTrigger id={id} title={title} />
<MenubarList id={id}>{children}</MenubarList>
</li>
);
}

MenubarMenu.propTypes = {
id: PropTypes.string.isRequired,
title: PropTypes.node.isRequired,
children: PropTypes.node
};

MenubarMenu.defaultProps = {
children: null
};

export default MenubarMenu;
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ export const ParentMenuContext = createContext('none');

export const MenuOpenContext = createContext('none');

export const NavBarContext = createContext({
createDropdownHandlers: () => ({}),
export const MenubarContext = createContext({
createMenuHandlers: () => ({}),
createMenuItemHandlers: () => ({}),
toggleDropdownOpen: () => {}
toggleMenuOpen: () => {}
});
92 changes: 0 additions & 92 deletions client/components/Nav/NavBar.jsx

This file was deleted.

59 changes: 0 additions & 59 deletions client/components/Nav/NavDropdownMenu.jsx

This file was deleted.

Loading
Loading