diff --git a/client/components/Nav/NavBar.jsx b/client/components/Nav/NavBar.jsx index c8ae7c6377..5826613c81 100644 --- a/client/components/Nav/NavBar.jsx +++ b/client/components/Nav/NavBar.jsx @@ -1,13 +1,52 @@ 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); + 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); + }; + + // match focused item to typed character; if no match, focus is not moved + + 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]); @@ -34,6 +73,30 @@ function NavBar({ children, className }) { [setDropdownOpen] ); + 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) => ({ @@ -61,9 +124,16 @@ function NavBar({ children, className }) { setDropdownOpen(dropdown); } }), - toggleDropdownOpen + toggleDropdownOpen, + menuItems }), - [setDropdownOpen, toggleDropdownOpen, clearHideTimeout, handleBlur] + [ + setDropdownOpen, + toggleDropdownOpen, + clearHideTimeout, + handleBlur, + menuItems + ] ); return ( @@ -71,7 +141,9 @@ function NavBar({ children, className }) { <header> <div className={className} ref={nodeRef}> <MenuOpenContext.Provider value={dropdownOpen}> - {children} + <ul className="nav__items-left" role="menubar"> + {children} + </ul> </MenuOpenContext.Provider> </div> </header> diff --git a/client/components/Nav/NavDropdownMenu.jsx b/client/components/Nav/NavDropdownMenu.jsx index d2c5744c46..c120401f6a 100644 --- a/client/components/Nav/NavDropdownMenu.jsx +++ b/client/components/Nav/NavDropdownMenu.jsx @@ -1,8 +1,36 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; -import React, { useContext, useMemo } from 'react'; +import React, { + useContext, + useRef, + useEffect, + useMemo, + useState, + useReducer, + useCallback +} from 'react'; import TriangleIcon from '../../images/down-filled-triangle.svg'; -import { MenuOpenContext, NavBarContext, ParentMenuContext } from './contexts'; +import { + MenuOpenContext, + NavBarContext, + ParentMenuContext, + SubmenuContext +} from './contexts'; +import useKeyDownHandlers from '../../common/useKeyDownHandlers'; + +const INIT_STATE = { + currentIndex: null, + prevIndex: null +}; + +function submenuReducer(state, { type, payload }) { + switch (type) { + case 'setIndex': + return { ...state, currentIndex: payload, prevIndex: state.currentIndex }; + default: + return state; + } +} export function useMenuProps(id) { const activeMenu = useContext(MenuOpenContext); @@ -19,30 +47,164 @@ export function useMenuProps(id) { return { isOpen, handlers }; } -function NavDropdownMenu({ id, title, children }) { +function NavTrigger({ id, title, ...props }) { + const submenuContext = useContext(SubmenuContext); const { isOpen, handlers } = useMenuProps(id); + const { isFirstChild, first } = submenuContext; + + useKeyDownHandlers({ + Space: (e) => { + e.preventDefault(); + e.stopPropagation(); + first(); + }, + ArrowDown: (e) => { + // open the menu and focus on the first item + } + // handle match to char keys + }); + + const triggerProps = { + ...handlers, + ...props, + role: 'menuitem', + 'aria-haspopup': 'menu', + 'aria-expanded': isOpen, + tabIndex: isFirstChild ? 0 : -1 + }; + + return ( + <button {...triggerProps}> + <span className="nav__item-header">{title}</span> + <TriangleIcon + className="nav__item-header-triangle" + focusable="false" + aria-hidden="true" + /> + </button> + ); +} + +NavTrigger.propTypes = { + id: PropTypes.string.isRequired, + title: PropTypes.node.isRequired +}; + +function NavList({ children, id }) { + const submenuContext = useContext(SubmenuContext); + + const { submenuItems, currentIndex, dispatch } = submenuContext; + + const prev = () => { + const index = currentIndex === 0 ? submenuItems.size - 1 : currentIndex - 1; + dispatch({ type: 'setIndex', payload: index }); + }; + + const next = () => { + const index = currentIndex === submenuItems.size - 1 ? 0 : currentIndex + 1; + dispatch({ type: 'setIndex', payload: index }); + }; + + useKeyDownHandlers({ + ArrowUp: (e) => { + e.preventDefault(); + e.stopPropagation(); + prev(); + }, + ArrowDown: (e) => { + e.preventDefault(); + e.stopPropagation(); + next(); + } + // keydown event listener for letter keys + }); + + const listProps = { + role: 'menu' + }; + + return ( + <ul className="nav__dropdown" {...listProps}> + <ParentMenuContext.Provider value={id}> + {children} + </ParentMenuContext.Provider> + </ul> + ); +} + +NavList.propTypes = { + id: PropTypes.string.isRequired, + children: PropTypes.node +}; + +NavList.defaultProps = { + children: null +}; + +function NavDropdownMenu({ id, title, children }) { + const { isOpen } = useMenuProps(id); + const [isFirstChild, setIsFirstChild] = useState(false); + const menuItemRef = useRef(); + const { menuItems } = useContext(NavBarContext); + const submenuItems = useRef(new Set()).current; + const [state, dispatch] = useReducer(submenuReducer, INIT_STATE); + const { currentIndex, prevIndex } = state; + + const first = useCallback(() => { + dispatch({ type: 'setIndex', payload: 0 }); + }, []); + + const last = useCallback( + () => dispatch({ type: 'setIndex', payload: submenuItems.size - 1 }), + [submenuItems.size] + ); + + useEffect(() => { + const menuItemNode = menuItemRef.current; + if (menuItemNode) { + if (!menuItems.size) { + setIsFirstChild(true); + } + menuItems.add(menuItemNode); + } + + return () => { + menuItems.delete(menuItemNode); + }; + }, [menuItems]); + + useEffect(() => { + const items = Array.from(submenuItems); + + if (currentIndex !== prevIndex) { + const currentNode = items[currentIndex]?.firstChild; + currentNode?.focus(); + } + }, [submenuItems, currentIndex, prevIndex]); + + const value = useMemo( + () => ({ + isFirstChild, + submenuItems, + currentIndex, + dispatch, + first, + last + }), + [isFirstChild, submenuItems, currentIndex, first, last] + ); + return ( - <li className={classNames('nav__item', isOpen && 'nav__item--open')}> - <button - {...handlers} - role="menuitem" - aria-haspopup="menu" - aria-expanded={isOpen} + <SubmenuContext.Provider value={value}> + <li + className={classNames('nav__item', isOpen && 'nav__item--open')} + ref={menuItemRef} > - <span className="nav__item-header">{title}</span> - <TriangleIcon - className="nav__item-header-triangle" - focusable="false" - aria-hidden="true" - /> - </button> - <ul className="nav__dropdown" role="menu"> - <ParentMenuContext.Provider value={id}> - {children} - </ParentMenuContext.Provider> - </ul> - </li> + <NavTrigger id={id} title={title} /> + <NavList id={id}>{children}</NavList> + </li> + </SubmenuContext.Provider> ); } diff --git a/client/components/Nav/NavMenuItem.jsx b/client/components/Nav/NavMenuItem.jsx index 09436e43ee..812633fae7 100644 --- a/client/components/Nav/NavMenuItem.jsx +++ b/client/components/Nav/NavMenuItem.jsx @@ -1,25 +1,51 @@ import PropTypes from 'prop-types'; -import React, { useContext, useMemo } from 'react'; +import React, { useContext, useMemo, useState, useRef, useEffect } from 'react'; import ButtonOrLink from '../../common/ButtonOrLink'; -import { NavBarContext, ParentMenuContext } from './contexts'; +import { NavBarContext, ParentMenuContext, SubmenuContext } from './contexts'; function NavMenuItem({ hideIf, className, ...rest }) { + const [isFirstChild, setIsFirstChild] = useState(false); + const menuItemRef = useRef(null); + const menubarContext = useContext(NavBarContext); + const submenuContext = useContext(SubmenuContext); + const { submenuItems } = submenuContext; const parent = useContext(ParentMenuContext); - const { createMenuItemHandlers } = useContext(NavBarContext); + const { createMenuItemHandlers } = menubarContext; const handlers = useMemo(() => createMenuItemHandlers(parent), [ createMenuItemHandlers, parent ]); + useEffect(() => { + const menuItemNode = menuItemRef.current; + if (menuItemNode) { + if (!submenuItems?.size) { + setIsFirstChild(true); + } + submenuItems?.add(menuItemNode); + } + + return () => { + submenuItems?.delete(menuItemNode); + }; + }, [submenuItems]); + if (hideIf) { return null; } + const buttonProps = { + ...rest, + ...handlers, + role: 'menuitem', + tabIndex: !submenuContext && isFirstChild ? 0 : -1 + }; + return ( - <li className={className}> - <ButtonOrLink {...rest} {...handlers} role="menuitem" /> + <li className={className} ref={menuItemRef}> + <ButtonOrLink {...buttonProps} /> </li> ); } diff --git a/client/components/Nav/contexts.jsx b/client/components/Nav/contexts.jsx index 896d7283f4..202011b5cc 100644 --- a/client/components/Nav/contexts.jsx +++ b/client/components/Nav/contexts.jsx @@ -2,6 +2,8 @@ import { createContext } from 'react'; export const ParentMenuContext = createContext('none'); +export const SubmenuContext = createContext('none'); + export const MenuOpenContext = createContext('none'); export const NavBarContext = createContext({ diff --git a/client/modules/IDE/components/Header/Nav.jsx b/client/modules/IDE/components/Header/Nav.jsx index 3492c4388a..4ad4a6c86d 100644 --- a/client/modules/IDE/components/Header/Nav.jsx +++ b/client/modules/IDE/components/Header/Nav.jsx @@ -37,10 +37,12 @@ const Nav = ({ layout }) => { return isMobile ? ( <MobileNav /> ) : ( - <NavBar> - <LeftLayout layout={layout} /> + <> + <NavBar> + <LeftLayout layout={layout} /> + </NavBar> <UserMenu /> - </NavBar> + </> ); }; @@ -137,7 +139,7 @@ const ProjectMenu = () => { metaKey === 'Ctrl' ? `${metaKeyName}+Alt+N` : `${metaKeyName}+⌥+N`; return ( - <ul className="nav__items-left" role="menubar"> + <> <li className="nav__item-logo"> {user && user.username !== undefined ? ( <Link to={userSketches}> @@ -247,7 +249,8 @@ const ProjectMenu = () => { </NavMenuItem> <NavMenuItem href="/about">{t('Nav.Help.About')}</NavMenuItem> </NavDropdownMenu> - </ul> + {getConfig('TRANSLATIONS_ENABLED') && <LanguageMenu />} + </> ); }; @@ -276,9 +279,8 @@ 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> @@ -286,7 +288,7 @@ const UnauthenticatedUserMenu = () => { </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> diff --git a/client/modules/IDE/components/Header/__snapshots__/Nav.unit.test.jsx.snap b/client/modules/IDE/components/Header/__snapshots__/Nav.unit.test.jsx.snap index af56a1a418..1f1f887f91 100644 --- a/client/modules/IDE/components/Header/__snapshots__/Nav.unit.test.jsx.snap +++ b/client/modules/IDE/components/Header/__snapshots__/Nav.unit.test.jsx.snap @@ -8,36 +8,41 @@ exports[`Nav renders dashboard version for desktop 1`] = ` > <ul class="nav__items-left" + role="menubar" > - <li - class="nav__item-logo" - > - <test-file-stub - aria-label="p5.js Logo" - classname="svg__logo" - focusable="false" - role="img" - /> - </li> - <li - class="nav__item nav__item--no-icon" + <ul + class="nav__items-left" > - <a - class="nav__back-link" - href="/" + <li + class="nav__item-logo" > <test-file-stub - aria-hidden="true" - classname="nav__back-icon" + aria-label="p5.js Logo" + classname="svg__logo" focusable="false" + role="img" /> - <span - class="nav__item-header" + </li> + <li + class="nav__item nav__item--no-icon" + > + <a + class="nav__back-link" + href="/" > - Back to Editor - </span> - </a> - </li> + <test-file-stub + aria-hidden="true" + classname="nav__back-icon" + focusable="false" + /> + <span + class="nav__item-header" + > + Back to Editor + </span> + </a> + </li> + </ul> </ul> </div> </header> @@ -269,51 +274,71 @@ exports[`Nav renders dashboard version for mobile 1`] = ` <div class="c0" > - <div - class="c1" - > - <test-file-stub /> - </div> - <div - class="c2" + <ul + class="nav__items-left" + role="menubar" > - <h1> - <span - class="editable-input editable-input--is-not-editing editable-input--has-value " - > - <button - aria-hidden="false" - aria-label="Edit sketch name" - class="editable-input__label" + <div + class="c1" + > + <test-file-stub /> + </div> + <div + class="c2" + > + <h1> + <span + class="editable-input editable-input--is-not-editing editable-input--has-value " > - <span> - Test project name - </span> - <test-file-stub + <button + aria-hidden="false" + aria-label="Edit sketch name" + class="editable-input__label" + > + <span> + Test project name + </span> + <test-file-stub + aria-hidden="true" + classname="editable-input__icon" + focusable="false" + /> + </button> + <input aria-hidden="true" - classname="editable-input__icon" - focusable="false" + aria-label="New sketch name" + class="editable-input__input" + disabled="" + maxlength="128" + type="text" + value="Test project name" /> - </button> - <input - aria-hidden="true" - aria-label="New sketch name" - class="editable-input__input" - disabled="" - maxlength="128" - type="text" - value="Test project name" - /> - </span> - </h1> - </div> - <div - class="c3" - > - <div> - <a - href="/login" - > + </span> + </h1> + </div> + <div + class="c3" + > + <div> + <a + href="/login" + > + <button + class="c4 c5" + display="inline" + focusable="false" + kind="primary" + type="button" + > + <test-file-stub + aria-hidden="true" + classname="sc-fujyAs cSTVlM" + focusable="false" + /> + </button> + </a> + </div> + <div> <button class="c4 c5" display="inline" @@ -323,160 +348,157 @@ exports[`Nav renders dashboard version for mobile 1`] = ` > <test-file-stub aria-hidden="true" - classname="sc-fujyAs cSTVlM" + classname="sc-iqAclL iOZiVo" focusable="false" /> </button> - </a> - </div> - <div> - <button - class="c4 c5" - display="inline" - focusable="false" - kind="primary" - type="button" - > - <test-file-stub - aria-hidden="true" - classname="sc-iqAclL iOZiVo" - focusable="false" - /> - </button> - <ul - class="" - > - <b> - File - </b> - <li - class="nav__dropdown-item" + <ul + class="" > - <button - role="menuitem" + <b> + File + </b> + <li + class="nav__dropdown-item" > - New - </button> - </li> - <li - class="nav__dropdown-item" - > - <button - role="menuitem" + <button + role="menuitem" + tabindex="-1" + > + New + </button> + </li> + <li + class="nav__dropdown-item" > - Save - </button> - </li> - <li - class="nav__dropdown-item" - > - <a - href="/p5/sketches" - role="menuitem" + <button + role="menuitem" + tabindex="-1" + > + Save + </button> + </li> + <li + class="nav__dropdown-item" > - Examples - </a> - </li> - <b> - Edit - </b> - <li - class="nav__dropdown-item" - > - <button - role="menuitem" + <a + href="/p5/sketches" + role="menuitem" + tabindex="-1" + > + Examples + </a> + </li> + <b> + Edit + </b> + <li + class="nav__dropdown-item" > - Tidy Code - </button> - </li> - <li - class="nav__dropdown-item" - > - <button - role="menuitem" + <button + role="menuitem" + tabindex="-1" + > + Tidy Code + </button> + </li> + <li + class="nav__dropdown-item" > - Find - </button> - </li> - <b> - Sketch - </b> - <li - class="nav__dropdown-item" - > - <button - role="menuitem" + <button + role="menuitem" + tabindex="-1" + > + Find + </button> + </li> + <b> + Sketch + </b> + <li + class="nav__dropdown-item" > - Add File - </button> - </li> - <li - class="nav__dropdown-item" - > - <button - role="menuitem" + <button + role="menuitem" + tabindex="-1" + > + Add File + </button> + </li> + <li + class="nav__dropdown-item" > - Add Folder - </button> - </li> - <b> - Settings - </b> - <li - class="nav__dropdown-item" - > - <button - role="menuitem" + <button + role="menuitem" + tabindex="-1" + > + Add Folder + </button> + </li> + <b> + Settings + </b> + <li + class="nav__dropdown-item" > - Preferences - </button> - </li> - <li - class="nav__dropdown-item" - > - <button - role="menuitem" + <button + role="menuitem" + tabindex="-1" + > + Preferences + </button> + </li> + <li + class="nav__dropdown-item" > - Language - </button> - </li> - <b> - Help - </b> - <li - class="nav__dropdown-item" - > - <button - role="menuitem" + <button + role="menuitem" + tabindex="-1" + > + Language + </button> + </li> + <b> + Help + </b> + <li + class="nav__dropdown-item" > - Keyboard Shortcuts - </button> - </li> - <li - class="nav__dropdown-item" - > - <a - href="https://p5js.org/reference/" - rel="noopener noreferrer" - role="menuitem" - target="_blank" + <button + role="menuitem" + tabindex="-1" + > + Keyboard Shortcuts + </button> + </li> + <li + class="nav__dropdown-item" > - Reference - </a> - </li> - <li - class="nav__dropdown-item" - > - <a - href="/about" - role="menuitem" + <a + href="https://p5js.org/reference/" + rel="noopener noreferrer" + role="menuitem" + tabindex="-1" + target="_blank" + > + Reference + </a> + </li> + <li + class="nav__dropdown-item" > - About - </a> - </li> - </ul> + <a + href="/about" + role="menuitem" + tabindex="-1" + > + About + </a> + </li> + </ul> + </div> </div> - </div> + </ul> </div> </header> </DocumentFragment> @@ -513,6 +535,7 @@ exports[`Nav renders editor version for desktop 1`] = ` aria-expanded="false" aria-haspopup="menu" role="menuitem" + tabindex="0" > <span class="nav__item-header" @@ -534,6 +557,7 @@ exports[`Nav renders editor version for desktop 1`] = ` > <button role="menuitem" + tabindex="-1" > New </button> @@ -547,6 +571,7 @@ exports[`Nav renders editor version for desktop 1`] = ` aria-expanded="false" aria-haspopup="menu" role="menuitem" + tabindex="-1" > <span class="nav__item-header" @@ -568,6 +593,7 @@ exports[`Nav renders editor version for desktop 1`] = ` > <button role="menuitem" + tabindex="-1" > Tidy Code <span @@ -582,6 +608,7 @@ exports[`Nav renders editor version for desktop 1`] = ` > <button role="menuitem" + tabindex="-1" > Find <span @@ -596,6 +623,7 @@ exports[`Nav renders editor version for desktop 1`] = ` > <button role="menuitem" + tabindex="-1" > Replace <span @@ -614,6 +642,7 @@ exports[`Nav renders editor version for desktop 1`] = ` aria-expanded="false" aria-haspopup="menu" role="menuitem" + tabindex="-1" > <span class="nav__item-header" @@ -635,6 +664,7 @@ exports[`Nav renders editor version for desktop 1`] = ` > <button role="menuitem" + tabindex="-1" > Add File <span @@ -649,6 +679,7 @@ exports[`Nav renders editor version for desktop 1`] = ` > <button role="menuitem" + tabindex="-1" > Add Folder </button> @@ -658,6 +689,7 @@ exports[`Nav renders editor version for desktop 1`] = ` > <button role="menuitem" + tabindex="-1" > Run <span @@ -672,6 +704,7 @@ exports[`Nav renders editor version for desktop 1`] = ` > <button role="menuitem" + tabindex="-1" > Stop <span @@ -690,6 +723,7 @@ exports[`Nav renders editor version for desktop 1`] = ` aria-expanded="false" aria-haspopup="menu" role="menuitem" + tabindex="-1" > <span class="nav__item-header" @@ -711,6 +745,7 @@ exports[`Nav renders editor version for desktop 1`] = ` > <button role="menuitem" + tabindex="-1" > Keyboard Shortcuts </button> @@ -722,6 +757,7 @@ exports[`Nav renders editor version for desktop 1`] = ` href="https://p5js.org/reference/" rel="noopener noreferrer" role="menuitem" + tabindex="-1" target="_blank" > Reference @@ -733,6 +769,7 @@ exports[`Nav renders editor version for desktop 1`] = ` <a href="/about" role="menuitem" + tabindex="-1" > About </a> @@ -970,51 +1007,71 @@ exports[`Nav renders editor version for mobile 1`] = ` <div class="c0" > - <div - class="c1" - > - <test-file-stub /> - </div> - <div - class="c2" + <ul + class="nav__items-left" + role="menubar" > - <h1> - <span - class="editable-input editable-input--is-not-editing editable-input--has-value " - > - <button - aria-hidden="false" - aria-label="Edit sketch name" - class="editable-input__label" + <div + class="c1" + > + <test-file-stub /> + </div> + <div + class="c2" + > + <h1> + <span + class="editable-input editable-input--is-not-editing editable-input--has-value " > - <span> - Test project name - </span> - <test-file-stub + <button + aria-hidden="false" + aria-label="Edit sketch name" + class="editable-input__label" + > + <span> + Test project name + </span> + <test-file-stub + aria-hidden="true" + classname="editable-input__icon" + focusable="false" + /> + </button> + <input aria-hidden="true" - classname="editable-input__icon" - focusable="false" + aria-label="New sketch name" + class="editable-input__input" + disabled="" + maxlength="128" + type="text" + value="Test project name" /> - </button> - <input - aria-hidden="true" - aria-label="New sketch name" - class="editable-input__input" - disabled="" - maxlength="128" - type="text" - value="Test project name" - /> - </span> - </h1> - </div> - <div - class="c3" - > - <div> - <a - href="/login" - > + </span> + </h1> + </div> + <div + class="c3" + > + <div> + <a + href="/login" + > + <button + class="c4 c5" + display="inline" + focusable="false" + kind="primary" + type="button" + > + <test-file-stub + aria-hidden="true" + classname="sc-fujyAs cSTVlM" + focusable="false" + /> + </button> + </a> + </div> + <div> <button class="c4 c5" display="inline" @@ -1024,160 +1081,157 @@ exports[`Nav renders editor version for mobile 1`] = ` > <test-file-stub aria-hidden="true" - classname="sc-fujyAs cSTVlM" + classname="sc-iqAclL iOZiVo" focusable="false" /> </button> - </a> - </div> - <div> - <button - class="c4 c5" - display="inline" - focusable="false" - kind="primary" - type="button" - > - <test-file-stub - aria-hidden="true" - classname="sc-iqAclL iOZiVo" - focusable="false" - /> - </button> - <ul - class="" - > - <b> - File - </b> - <li - class="nav__dropdown-item" + <ul + class="" > - <button - role="menuitem" + <b> + File + </b> + <li + class="nav__dropdown-item" > - New - </button> - </li> - <li - class="nav__dropdown-item" - > - <button - role="menuitem" + <button + role="menuitem" + tabindex="-1" + > + New + </button> + </li> + <li + class="nav__dropdown-item" > - Save - </button> - </li> - <li - class="nav__dropdown-item" - > - <a - href="/p5/sketches" - role="menuitem" + <button + role="menuitem" + tabindex="-1" + > + Save + </button> + </li> + <li + class="nav__dropdown-item" > - Examples - </a> - </li> - <b> - Edit - </b> - <li - class="nav__dropdown-item" - > - <button - role="menuitem" + <a + href="/p5/sketches" + role="menuitem" + tabindex="-1" + > + Examples + </a> + </li> + <b> + Edit + </b> + <li + class="nav__dropdown-item" > - Tidy Code - </button> - </li> - <li - class="nav__dropdown-item" - > - <button - role="menuitem" + <button + role="menuitem" + tabindex="-1" + > + Tidy Code + </button> + </li> + <li + class="nav__dropdown-item" > - Find - </button> - </li> - <b> - Sketch - </b> - <li - class="nav__dropdown-item" - > - <button - role="menuitem" + <button + role="menuitem" + tabindex="-1" + > + Find + </button> + </li> + <b> + Sketch + </b> + <li + class="nav__dropdown-item" > - Add File - </button> - </li> - <li - class="nav__dropdown-item" - > - <button - role="menuitem" + <button + role="menuitem" + tabindex="-1" + > + Add File + </button> + </li> + <li + class="nav__dropdown-item" > - Add Folder - </button> - </li> - <b> - Settings - </b> - <li - class="nav__dropdown-item" - > - <button - role="menuitem" + <button + role="menuitem" + tabindex="-1" + > + Add Folder + </button> + </li> + <b> + Settings + </b> + <li + class="nav__dropdown-item" > - Preferences - </button> - </li> - <li - class="nav__dropdown-item" - > - <button - role="menuitem" + <button + role="menuitem" + tabindex="-1" + > + Preferences + </button> + </li> + <li + class="nav__dropdown-item" > - Language - </button> - </li> - <b> - Help - </b> - <li - class="nav__dropdown-item" - > - <button - role="menuitem" + <button + role="menuitem" + tabindex="-1" + > + Language + </button> + </li> + <b> + Help + </b> + <li + class="nav__dropdown-item" > - Keyboard Shortcuts - </button> - </li> - <li - class="nav__dropdown-item" - > - <a - href="https://p5js.org/reference/" - rel="noopener noreferrer" - role="menuitem" - target="_blank" + <button + role="menuitem" + tabindex="-1" + > + Keyboard Shortcuts + </button> + </li> + <li + class="nav__dropdown-item" > - Reference - </a> - </li> - <li - class="nav__dropdown-item" - > - <a - href="/about" - role="menuitem" + <a + href="https://p5js.org/reference/" + rel="noopener noreferrer" + role="menuitem" + tabindex="-1" + target="_blank" + > + Reference + </a> + </li> + <li + class="nav__dropdown-item" > - About - </a> - </li> - </ul> + <a + href="/about" + role="menuitem" + tabindex="-1" + > + About + </a> + </li> + </ul> + </div> </div> - </div> + </ul> </div> </header> </DocumentFragment> diff --git a/client/modules/IDE/hooks/usePrevious.js b/client/modules/IDE/hooks/usePrevious.js new file mode 100644 index 0000000000..4e67dbb23c --- /dev/null +++ b/client/modules/IDE/hooks/usePrevious.js @@ -0,0 +1,12 @@ +/* https://usehooks.com/usePrevious/ */ +import { useRef, useEffect } from 'react'; + +export default function usePrevious(value) { + const ref = useRef(); + + useEffect(() => { + ref.current = value; + }, [value]); + + return ref.current; +}