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

Add keyboard behaviors to menubar #3220

Draft
wants to merge 21 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b9535b3
feat: adding usePrevious hook
tespin Jul 26, 2024
919b94b
feat: adding roving focus implementation
tespin Jul 26, 2024
3706d60
fix: tab focus is tracked in menubar instead of menuitem
tespin Aug 7, 2024
9d15664
fix: removed menuitem role from login and signup, moved language drop…
tespin Aug 8, 2024
be4ba67
feat: change focused item with based on directional keys
tespin Aug 8, 2024
0c98e43
chore: cleanup
tespin Aug 8, 2024
04e26e0
chore: cleanup again
tespin Aug 8, 2024
c612b31
fix: updated snapshots
tespin Aug 15, 2024
5b00ec3
Merge branch 'develop' into tespin/add-menubar-keyboard-interactions
tespin Aug 15, 2024
5545111
Merge branch 'develop' into tespin/add-menubar-keyboard-interactions
raclim Aug 22, 2024
0a5e398
Merge branch 'processing:develop' into tespin/add-menubar-keyboard-in…
tespin Aug 23, 2024
097dab4
chore: made separate trigger component for menubar
tespin Aug 21, 2024
8c631fb
adding submenu context
tespin Aug 22, 2024
572d9ef
chore: edit to trigger component, added list component for dropdown menu
tespin Aug 22, 2024
c8770f5
fix: moved user menu out of navbar
tespin Aug 22, 2024
04d5e12
feat: crefactored, added functionality to implement roving tab index
tespin Aug 23, 2024
530147f
chore: refacted to include ul element and methods for navigating menu…
tespin Aug 23, 2024
c5be1e7
feat: refactored to separate components, implemented navigating list …
tespin Aug 23, 2024
697b19c
fix: optional chaining for mobile view
tespin Aug 23, 2024
0eac300
fix: snapshots updated
tespin Aug 23, 2024
654889e
fix: fixed lint errors
tespin Aug 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 96 additions & 4 deletions client/components/Nav/NavBar.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
import PropTypes from 'prop-types';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import useModalClose from '../../common/useModalClose';
import { MenuOpenContext, NavBarContext } from './contexts';
import usePrevious from '../../modules/IDE/hooks/usePrevious';
import useKeyDownHandlers from '../../common/useKeyDownHandlers';

function NavBar({ children, className }) {
const [dropdownOpen, setDropdownOpen] = useState('none');

const [currentIndex, setCurrentIndex] = useState(0);
const prevIndex = usePrevious(currentIndex) ?? null;
const menuItems = useRef(new Set()).current;
const timerRef = useRef(null);

useEffect(() => {
if (currentIndex !== prevIndex) {
const items = Array.from(menuItems);
const currentNode = items[currentIndex]?.firstChild;
const prevNode = items[prevIndex]?.firstChild;

prevNode?.setAttribute('tabindex', -1);
currentNode?.setAttribute('tabindex', 0);
currentNode?.focus();
}
}, [currentIndex, prevIndex, menuItems]);

const handleClose = useCallback(() => {
setDropdownOpen('none');
}, [setDropdownOpen]);
Expand All @@ -34,6 +56,69 @@
[setDropdownOpen]
);

const first = () => {
setCurrentIndex(0);
};
const last = () => {
setCurrentIndex(menuItems.size - 1);
};
const next = () => {
const index = currentIndex === menuItems.size - 1 ? 0 : currentIndex + 1;
setCurrentIndex(index);
};
const prev = () => {
const index = currentIndex === 0 ? menuItems.size - 1 : currentIndex - 1;
setCurrentIndex(index);
};
const match = (e) => {

Check warning on line 73 in client/components/Nav/NavBar.jsx

View workflow job for this annotation

GitHub Actions / Test and lint code base

'match' is assigned a value but never used
console.log(e.key);
const items = Array.from(menuItems);

const reorderedItems = [
...items.slice(currentIndex),
...items.slice(0, currentIndex)
];

const matches = reorderedItems.filter((menuItem) => {
const { textContent } = menuItem.firstChild;
const firstChar = textContent[0].toLowerCase();
return e.key === firstChar;
});

if (!matches.length) {
return;
}

const currentNode = items[currentIndex];
const nextMatch = matches.includes(currentNode) ? matches[1] : matches[0];
const index = items.findIndex((item) => item === nextMatch);
setCurrentIndex(index);
};

useKeyDownHandlers({
ArrowLeft: (e) => {
e.preventDefault();
e.stopPropagation();
prev();
},
ArrowRight: (e) => {
e.preventDefault();
e.stopPropagation();
next();
},
Home: (e) => {
e.preventDefault();
e.stopPropagation();
first();
},
End: (e) => {
e.preventDefault();
e.stopPropagation();
last();
}
// keydown event listener for letter keys
});

const contextValue = useMemo(
() => ({
createDropdownHandlers: (dropdown) => ({
Expand Down Expand Up @@ -61,9 +146,16 @@
setDropdownOpen(dropdown);
}
}),
toggleDropdownOpen
toggleDropdownOpen,
menuItems
}),
[setDropdownOpen, toggleDropdownOpen, clearHideTimeout, handleBlur]
[
setDropdownOpen,
toggleDropdownOpen,
clearHideTimeout,
handleBlur,
menuItems
]
);

return (
Expand Down
25 changes: 23 additions & 2 deletions client/components/Nav/NavDropdownMenu.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { useContext, useMemo } from 'react';
import React, { useContext, useRef, useEffect, useMemo, useState } from 'react';
import TriangleIcon from '../../images/down-filled-triangle.svg';
import { MenuOpenContext, NavBarContext, ParentMenuContext } from './contexts';

Expand All @@ -21,14 +21,35 @@ export function useMenuProps(id) {

function NavDropdownMenu({ id, title, children }) {
const { isOpen, handlers } = useMenuProps(id);
const [isFirstChild, setIsFirstChild] = useState(false);
const menuItemRef = useRef();
const { menuItems } = useContext(NavBarContext);

useEffect(() => {
const menuItemNode = menuItemRef.current;
if (menuItemNode) {
if (!menuItems.size) {
setIsFirstChild(true);
}
menuItems.add(menuItemNode);
}

return () => {
menuItems.delete(menuItemNode);
};
}, [menuItems]);

return (
<li className={classNames('nav__item', isOpen && 'nav__item--open')}>
<li
className={classNames('nav__item', isOpen && 'nav__item--open')}
ref={menuItemRef}
>
<button
{...handlers}
role="menuitem"
aria-haspopup="menu"
aria-expanded={isOpen}
tabIndex={isFirstChild ? 0 : -1}
>
<span className="nav__item-header">{title}</span>
<TriangleIcon
Expand Down
6 changes: 3 additions & 3 deletions client/modules/IDE/components/Header/Nav.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ const ProjectMenu = () => {
</NavMenuItem>
<NavMenuItem href="/about">{t('Nav.Help.About')}</NavMenuItem>
</NavDropdownMenu>
{getConfig('TRANSLATIONS_ENABLED') && <LanguageMenu />}
</ul>
);
};
Expand Down Expand Up @@ -276,17 +277,16 @@ const UnauthenticatedUserMenu = () => {
const { t } = useTranslation();
return (
<ul className="nav__items-right" title="user-menu" role="navigation">
{getConfig('TRANSLATIONS_ENABLED') && <LanguageMenu />}
<li className="nav__item">
<Link to="/login" className="nav__auth-button" role="menuitem">
<Link to="/login" className="nav__auth-button">
<span className="nav__item-header" title="Login">
{t('Nav.Login')}
</span>
</Link>
</li>
<li className="nav__item-or">{t('Nav.LoginOr')}</li>
<li className="nav__item">
<Link to="/signup" className="nav__auth-button" role="menuitem">
<Link to="/signup" className="nav__auth-button">
<span className="nav__item-header" title="SignUp">
{t('Nav.SignUp')}
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ exports[`Nav renders editor version for desktop 1`] = `
aria-expanded="false"
aria-haspopup="menu"
role="menuitem"
tabindex="0"
>
<span
class="nav__item-header"
Expand Down Expand Up @@ -547,6 +548,7 @@ exports[`Nav renders editor version for desktop 1`] = `
aria-expanded="false"
aria-haspopup="menu"
role="menuitem"
tabindex="-1"
>
<span
class="nav__item-header"
Expand Down Expand Up @@ -614,6 +616,7 @@ exports[`Nav renders editor version for desktop 1`] = `
aria-expanded="false"
aria-haspopup="menu"
role="menuitem"
tabindex="-1"
>
<span
class="nav__item-header"
Expand Down Expand Up @@ -690,6 +693,7 @@ exports[`Nav renders editor version for desktop 1`] = `
aria-expanded="false"
aria-haspopup="menu"
role="menuitem"
tabindex="-1"
>
<span
class="nav__item-header"
Expand Down
12 changes: 12 additions & 0 deletions client/modules/IDE/hooks/usePrevious.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* https://usehooks.com/usePrevious/ */
import React, { useRef, useEffect } from 'react';

Check warning on line 2 in client/modules/IDE/hooks/usePrevious.js

View workflow job for this annotation

GitHub Actions / Test and lint code base

'React' is defined but never used

export default function usePrevious(value) {
const ref = useRef();

useEffect(() => {
ref.current = value;
}, [value]);

return ref.current;
}
Loading