Skip to content

Commit

Permalink
Merge branch 'develop' into about-redesign
Browse files Browse the repository at this point in the history
  • Loading branch information
raclim committed Jan 15, 2025
2 parents 78058d8 + a4ddf9b commit 2ce92d6
Show file tree
Hide file tree
Showing 11 changed files with 754 additions and 647 deletions.
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;
58 changes: 58 additions & 0 deletions client/components/Menubar/MenubarItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import PropTypes from 'prop-types';
import React, { useContext, useMemo } from 'react';
import ButtonOrLink from '../../common/ButtonOrLink';
import { MenubarContext, ParentMenuContext } from './contexts';

function MenubarItem({
hideIf,
className,
role: customRole,
selected,
...rest
}) {
const parent = useContext(ParentMenuContext);

const { createMenuItemHandlers } = useContext(MenubarContext);

const handlers = useMemo(() => createMenuItemHandlers(parent), [
createMenuItemHandlers,
parent
]);

if (hideIf) {
return null;
}

const role = customRole || 'menuitem';
const ariaSelected = role === 'option' ? { 'aria-selected': selected } : {};

return (
<li className={className}>
<ButtonOrLink {...rest} {...handlers} {...ariaSelected} role={role} />
</li>
);
}

MenubarItem.propTypes = {
...ButtonOrLink.propTypes,
onClick: PropTypes.func,
value: PropTypes.string,
/**
* Provides a way to deal with optional items.
*/
hideIf: PropTypes.bool,
className: PropTypes.string,
role: PropTypes.oneOf(['menuitem', 'option']),
selected: PropTypes.bool
};

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

export default MenubarItem;
135 changes: 135 additions & 0 deletions client/components/Menubar/MenubarSubmenu.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// https://blog.logrocket.com/building-accessible-menubar-component-react

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, role, hasPopup, ...props }) {
const { isOpen, handlers } = useMenuProps(id);

return (
<button
{...handlers}
{...props}
role={role}
aria-haspopup={hasPopup}
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,
role: PropTypes.string,
hasPopup: PropTypes.oneOf(['menu', 'listbox', 'true'])
};

MenubarTrigger.defaultProps = {
role: 'menuitem',
hasPopup: 'menu'
};

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

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

MenubarList.propTypes = {
id: PropTypes.string.isRequired,
children: PropTypes.node,
role: PropTypes.oneOf(['menu', 'listbox'])
};

MenubarList.defaultProps = {
children: null,
role: 'menu'
};

/* -------------------------------------------------------------------------------------------------
* MenubarSubmenu
* -----------------------------------------------------------------------------------------------*/

function MenubarSubmenu({
id,
title,
children,
triggerRole: customTriggerRole,
listRole: customListRole,
...props
}) {
const { isOpen } = useMenuProps(id);

const triggerRole = customTriggerRole || 'menuitem';
const listRole = customListRole || 'menu';

const hasPopup = listRole === 'listbox' ? 'listbox' : 'menu';

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

MenubarSubmenu.propTypes = {
id: PropTypes.string.isRequired,
title: PropTypes.node.isRequired,
children: PropTypes.node,
triggerRole: PropTypes.string,
listRole: PropTypes.string
};

MenubarSubmenu.defaultProps = {
children: null,
triggerRole: 'menuitem',
listRole: 'menu'
};

export default MenubarSubmenu;
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.

Loading

0 comments on commit 2ce92d6

Please sign in to comment.