From 016459486eac17d3c6f3fab48fad9bb21edce08f Mon Sep 17 00:00:00 2001 From: Tristan Espinoza Date: Wed, 15 Jan 2025 16:02:09 -0800 Subject: [PATCH 001/111] feat: added context values for keyboard navigation --- client/components/Menubar/contexts.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/components/Menubar/contexts.jsx b/client/components/Menubar/contexts.jsx index ab3bb9ffcf..f97c3ea8fe 100644 --- a/client/components/Menubar/contexts.jsx +++ b/client/components/Menubar/contexts.jsx @@ -1,3 +1,4 @@ +import { set } from 'lodash'; import { createContext } from 'react'; export const ParentMenuContext = createContext('none'); @@ -7,5 +8,9 @@ export const MenuOpenContext = createContext('none'); export const MenubarContext = createContext({ createMenuHandlers: () => ({}), createMenuItemHandlers: () => ({}), - toggleMenuOpen: () => {} + toggleMenuOpen: () => {}, + activeIndex: -1, + setActiveIndex: () => {}, + registerItem: () => {}, + menuItems: [] }); From 656b7405d28424440c6615d28aad0b292b703a29 Mon Sep 17 00:00:00 2001 From: Tristan Espinoza Date: Thu, 16 Jan 2025 00:10:41 -0800 Subject: [PATCH 002/111] feat: top-level keyboard navigation added --- client/components/Menubar/Menubar.jsx | 103 ++++++++++++++++--- client/components/Menubar/MenubarItem.jsx | 8 +- client/components/Menubar/MenubarSubmenu.jsx | 67 +++++++----- 3 files changed, 140 insertions(+), 38 deletions(-) diff --git a/client/components/Menubar/Menubar.jsx b/client/components/Menubar/Menubar.jsx index b806246515..21d10fbe44 100644 --- a/client/components/Menubar/Menubar.jsx +++ b/client/components/Menubar/Menubar.jsx @@ -2,18 +2,87 @@ import PropTypes from 'prop-types'; import React, { useCallback, useMemo, useRef, useState } from 'react'; import useModalClose from '../../common/useModalClose'; import { MenuOpenContext, MenubarContext } from './contexts'; +import useKeyDownHandlers from '../../common/useKeyDownHandlers'; function Menubar({ children, className }) { const [menuOpen, setMenuOpen] = useState('none'); - + const [activeIndex, setActiveIndex] = useState(-1); + const [menuItems, setMenuItems] = useState([]); const timerRef = useRef(null); + const nodeRef = useRef(null); + + const registerItem = useCallback((id) => { + setMenuItems((prev) => [...prev, id]); + return () => { + setMenuItems((prev) => prev.filter((item) => item !== id)); + }; + }, []); + + const toggleMenuOpen = useCallback( + (menu) => { + setMenuOpen((prevState) => (prevState === menu ? 'none' : menu)); + }, + [setMenuOpen] + ); + + const keyHandlers = useMemo( + () => ({ + ArrowUp: (e) => { + e.preventDefault(); + // if submenu is closed, open it and focus the last item + // if submenu is open, focus the previous item + }, + ArrowDown: (e) => { + e.preventDefault(); + + // if submenu is closed, open it and focus the first item + // if submenu is open, focus the next item + }, + ArrowLeft: (e) => { + e.preventDefault(); + console.log('left'); + setActiveIndex( + (prev) => (prev - 1 + menuItems.length) % menuItems.length + ); + // if submenu is open, close it, open the next one and focus the next top-level item + }, + ArrowRight: (e) => { + e.preventDefault(); + console.log('right'); + setActiveIndex((prev) => (prev + 1) % menuItems.length); + // if submenu is open, close it, open previous one and focus the previous top-level item + }, + Enter: (e) => { + e.preventDefault(); + // if submenu is open, activate the focused item + // if submenu is closed, open it and focus the first item + toggleMenuOpen(menuItems[activeIndex]); + }, + ' ': (e) => { + // same as Enter + e.preventDefault(); + // if submenu is open, activate the focused item + // if submenu is closed, open it and focus the first item + toggleMenuOpen(menuItems[activeIndex]); + }, + Escape: (e) => { + // close all submenus + setMenuOpen('none'); + }, + Tab: (e) => { + // close + } + // support direct access keys + }), + [menuItems, menuOpen, activeIndex, toggleMenuOpen] + ); + + useKeyDownHandlers(keyHandlers); const handleClose = useCallback(() => { setMenuOpen('none'); }, [setMenuOpen]); - const nodeRef = useModalClose(handleClose); - const clearHideTimeout = useCallback(() => { if (timerRef.current) { clearTimeout(timerRef.current); @@ -22,16 +91,12 @@ function Menubar({ children, className }) { }, [timerRef]); const handleBlur = useCallback(() => { - timerRef.current = setTimeout(() => setMenuOpen('none'), 10); + timerRef.current = setTimeout(() => { + setMenuOpen('none'); + setActiveIndex(-1); + }, 10); }, [timerRef, setMenuOpen]); - const toggleMenuOpen = useCallback( - (menu) => { - setMenuOpen((prevState) => (prevState === menu ? 'none' : menu)); - }, - [setMenuOpen] - ); - const contextValue = useMemo( () => ({ createMenuHandlers: (menu) => ({ @@ -57,9 +122,21 @@ function Menubar({ children, className }) { setMenuOpen(menu); } }), - toggleMenuOpen + toggleMenuOpen, + activeIndex, + setActiveIndex, + registerItem, + menuItems }), - [setMenuOpen, toggleMenuOpen, clearHideTimeout, handleBlur] + [ + menuOpen, + toggleMenuOpen, + clearHideTimeout, + handleBlur, + activeIndex, + registerItem, + menuItems + ] ); return ( diff --git a/client/components/Menubar/MenubarItem.jsx b/client/components/Menubar/MenubarItem.jsx index 8d595bb5cd..dcfebe60b7 100644 --- a/client/components/Menubar/MenubarItem.jsx +++ b/client/components/Menubar/MenubarItem.jsx @@ -28,7 +28,13 @@ function MenubarItem({ return (
  • - +
  • ); } diff --git a/client/components/Menubar/MenubarSubmenu.jsx b/client/components/Menubar/MenubarSubmenu.jsx index 13b0e33177..ef917d0ace 100644 --- a/client/components/Menubar/MenubarSubmenu.jsx +++ b/client/components/Menubar/MenubarSubmenu.jsx @@ -2,7 +2,7 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; -import React, { useContext, useMemo } from 'react'; +import React, { useEffect, useContext, useRef, useMemo } from 'react'; import TriangleIcon from '../../images/down-filled-triangle.svg'; import { MenuOpenContext, MenubarContext, ParentMenuContext } from './contexts'; @@ -24,27 +24,29 @@ export function useMenuProps(id) { /* ------------------------------------------------------------------------------------------------- * MenubarTrigger * -----------------------------------------------------------------------------------------------*/ - -function MenubarTrigger({ id, title, role, hasPopup, ...props }) { - const { isOpen, handlers } = useMenuProps(id); - - return ( - - ); -} +const MenubarTrigger = React.forwardRef( + ({ id, title, role, hasPopup, ...props }, ref) => { + const { isOpen, handlers } = useMenuProps(id); + + return ( + + ); + } +); MenubarTrigger.propTypes = { id: PropTypes.string.isRequired, @@ -95,20 +97,37 @@ function MenubarSubmenu({ listRole: customListRole, ...props }) { - const { isOpen } = useMenuProps(id); + const { isOpen, handlers } = useMenuProps(id); + const { activeIndex, menuItems, registerItem } = useContext(MenubarContext); + const isActive = menuItems[activeIndex] === id; + const buttonRef = useRef(null); const triggerRole = customTriggerRole || 'menuitem'; const listRole = customListRole || 'menu'; - const hasPopup = listRole === 'listbox' ? 'listbox' : 'menu'; + useEffect(() => { + if (isActive && buttonRef.current) { + buttonRef.current.focus(); + } + }, [isActive]); + + // register this menu item + useEffect(() => { + const unregister = registerItem(id); + return unregister; + }, [id, registerItem]); + return (
  • From ec66297e6aa0466d59ab15d8a0d4808e07fa9486 Mon Sep 17 00:00:00 2001 From: Tristan Espinoza Date: Thu, 16 Jan 2025 16:04:26 -0800 Subject: [PATCH 003/111] feat: implemented ref forwarding --- client/common/ButtonOrLink.jsx | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/client/common/ButtonOrLink.jsx b/client/common/ButtonOrLink.jsx index 924f108024..fa7258dff8 100644 --- a/client/common/ButtonOrLink.jsx +++ b/client/common/ButtonOrLink.jsx @@ -5,23 +5,34 @@ import PropTypes from 'prop-types'; /** * Helper for switching between ; -}; + return ( + + ); +}); /** * Accepts all the props of an HTML or
  • - {t('Nav.File.New')} + + {t('Nav.File.New')} + { {metaKeyName}+S dispatch(cloneProject())} > {t('Nav.File.Duplicate')} - + {t('Nav.File.Share')} - + {t('Nav.File.Download')} {t('Nav.File.Open')} { {t('Nav.File.AddToCollection')} @@ -210,32 +221,38 @@ const ProjectMenu = () => { - + {t('Nav.Edit.TidyCode')} {metaKeyName}+Shift+F - + {t('Nav.Edit.Find')} {metaKeyName}+F - + {t('Nav.Edit.Replace')} {replaceCommand} - dispatch(newFile(rootFile.id))}> + dispatch(newFile(rootFile.id))} + > {t('Nav.Sketch.AddFile')} {newFileCommand} - dispatch(newFolder(rootFile.id))}> + dispatch(newFolder(rootFile.id))} + > {t('Nav.Sketch.AddFolder')} - dispatch(startSketch())}> + dispatch(startSketch())}> {t('Nav.Sketch.Run')} {metaKeyName}+Enter - dispatch(stopSketch())}> + dispatch(stopSketch())}> {t('Nav.Sketch.Stop')} Shift+{metaKeyName}+Enter @@ -243,13 +260,18 @@ const ProjectMenu = () => { - dispatch(showKeyboardShortcutModal())}> + dispatch(showKeyboardShortcutModal())} + > {t('Nav.Help.KeyboardShortcuts')} - + {t('Nav.Help.Reference')} - {t('Nav.Help.About')} + + {t('Nav.Help.About')} + {getConfig('TRANSLATIONS_ENABLED') && } @@ -275,6 +297,7 @@ const LanguageMenu = () => { {sortBy(availableLanguages).map((key) => ( // eslint-disable-next-line react/jsx-no-bind { } > - + {t('Nav.Auth.MySketches')} {t('Nav.Auth.MyCollections')} - + {t('Nav.Auth.MyAssets')} - {t('Preferences.Settings')} - dispatch(logoutUser())}> + + {t('Preferences.Settings')} + + dispatch(logoutUser())}> {t('Nav.Auth.LogOut')} From 49e3ddb650681d4b0adb4c7858d13d522f42a2cb Mon Sep 17 00:00:00 2001 From: Tristan Espinoza Date: Thu, 16 Jan 2025 16:11:14 -0800 Subject: [PATCH 006/111] feat: updated with relevant submenu states and key handlers --- client/components/Menubar/Menubar.jsx | 56 ++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/client/components/Menubar/Menubar.jsx b/client/components/Menubar/Menubar.jsx index 21d10fbe44..3ae2bc0b0b 100644 --- a/client/components/Menubar/Menubar.jsx +++ b/client/components/Menubar/Menubar.jsx @@ -8,6 +8,8 @@ function Menubar({ children, className }) { const [menuOpen, setMenuOpen] = useState('none'); const [activeIndex, setActiveIndex] = useState(-1); const [menuItems, setMenuItems] = useState([]); + const [submenuActiveIndex, setSubmenuActiveIndex] = useState(-1); + const [submenuItems, setSubmenuItems] = useState([]); const timerRef = useRef(null); const nodeRef = useRef(null); @@ -18,6 +20,13 @@ function Menubar({ children, className }) { }; }, []); + const registerSubmenuItem = useCallback((id) => { + setSubmenuItems((prev) => [...prev, id]); + return () => { + setSubmenuItems((prev) => prev.filter((item) => item !== id)); + }; + }, []); + const toggleMenuOpen = useCallback( (menu) => { setMenuOpen((prevState) => (prevState === menu ? 'none' : menu)); @@ -30,27 +39,49 @@ function Menubar({ children, className }) { ArrowUp: (e) => { e.preventDefault(); // if submenu is closed, open it and focus the last item + if (menuOpen === 'none') { + toggleMenuOpen(menuItems[activeIndex]); + setSubmenuActiveIndex(submenuItems.length - 1); // focus last + } else { + setSubmenuActiveIndex( + (prev) => (prev - 1 + submenuItems.length) % submenuItems.length + ); + } // if submenu is open, focus the previous item }, ArrowDown: (e) => { e.preventDefault(); - // if submenu is closed, open it and focus the first item + if (menuOpen === 'none') { + toggleMenuOpen(menuItems[activeIndex]); + setSubmenuActiveIndex(0); // focus first + } else { + setSubmenuActiveIndex((prev) => (prev + 1) % submenuItems.length); + } // if submenu is open, focus the next item }, ArrowLeft: (e) => { e.preventDefault(); - console.log('left'); - setActiveIndex( - (prev) => (prev - 1 + menuItems.length) % menuItems.length - ); + const newIndex = + (activeIndex - 1 + menuItems.length) % menuItems.length; + setActiveIndex(newIndex); + // if submenu is open, close it, open the next one and focus the next top-level item + if (menuOpen !== 'none') { + toggleMenuOpen(menuItems[activeIndex]); + setMenuOpen(menuItems[newIndex]); + } }, ArrowRight: (e) => { e.preventDefault(); - console.log('right'); - setActiveIndex((prev) => (prev + 1) % menuItems.length); + const newIndex = (activeIndex + 1) % menuItems.length; + setActiveIndex(newIndex); + // if submenu is open, close it, open previous one and focus the previous top-level item + if (menuOpen !== 'none') { + toggleMenuOpen(menuItems[activeIndex]); + setMenuOpen(menuItems[newIndex]); + } }, Enter: (e) => { e.preventDefault(); @@ -126,7 +157,11 @@ function Menubar({ children, className }) { activeIndex, setActiveIndex, registerItem, - menuItems + menuItems, + submenuActiveIndex, + setSubmenuActiveIndex, + registerSubmenuItem, + submenuItems }), [ menuOpen, @@ -135,7 +170,10 @@ function Menubar({ children, className }) { handleBlur, activeIndex, registerItem, - menuItems + menuItems, + submenuActiveIndex, + registerSubmenuItem, + submenuItems ] ); From a300c5205040d8b27fc832e633fd3f79486720a9 Mon Sep 17 00:00:00 2001 From: Tristan Espinoza Date: Thu, 16 Jan 2025 16:18:52 -0800 Subject: [PATCH 007/111] feat: menubaritem registers self and determines if active or not --- client/components/Menubar/MenubarItem.jsx | 40 +++++++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/client/components/Menubar/MenubarItem.jsx b/client/components/Menubar/MenubarItem.jsx index dcfebe60b7..51aeea69ff 100644 --- a/client/components/Menubar/MenubarItem.jsx +++ b/client/components/Menubar/MenubarItem.jsx @@ -1,18 +1,25 @@ import PropTypes from 'prop-types'; -import React, { useContext, useMemo } from 'react'; +import React, { useEffect, useContext, useRef, useMemo } from 'react'; import ButtonOrLink from '../../common/ButtonOrLink'; import { MenubarContext, ParentMenuContext } from './contexts'; function MenubarItem({ + id, hideIf, className, role: customRole, selected, ...rest }) { + const submenuItemRef = useRef(null); const parent = useContext(ParentMenuContext); - const { createMenuItemHandlers } = useContext(MenubarContext); + const { + createMenuItemHandlers, + registerSubmenuItem, + submenuActiveIndex, + submenuItems + } = useContext(MenubarContext); const handlers = useMemo(() => createMenuItemHandlers(parent), [ createMenuItemHandlers, @@ -25,15 +32,41 @@ function MenubarItem({ const role = customRole || 'menuitem'; const ariaSelected = role === 'option' ? { 'aria-selected': selected } : {}; + const isActive = submenuItems[submenuActiveIndex] === id; + + useEffect(() => { + if (isActive && submenuItemRef.current) { + submenuItemRef.current.focus(); + } + }, [isActive, submenuActiveIndex]); + + useEffect(() => { + const unregister = registerSubmenuItem(id); + return unregister; + }, [id, registerSubmenuItem]); + + useEffect(() => { + if (isActive) { + console.log('MenubarItem Focus:', { + id, + isActive, + parent, + submenuActiveIndex, + element: submenuItemRef.current + }); + } + }, [isActive, id, parent, submenuActiveIndex]); return (
  • ); @@ -41,6 +74,7 @@ function MenubarItem({ MenubarItem.propTypes = { ...ButtonOrLink.propTypes, + id: PropTypes.string.isRequired, onClick: PropTypes.func, value: PropTypes.string, /** From 93f963d8740d14a6f287b128c7d940287ee2b184 Mon Sep 17 00:00:00 2001 From: Tristan Espinoza Date: Thu, 16 Jan 2025 23:19:56 -0800 Subject: [PATCH 008/111] feat: base implementation of keyboard-navigable submenus --- client/components/Menubar/MenubarItem.jsx | 16 ++- client/components/Menubar/MenubarSubmenu.jsx | 103 ++++++++++++++++++- client/components/Menubar/contexts.jsx | 4 +- 3 files changed, 108 insertions(+), 15 deletions(-) diff --git a/client/components/Menubar/MenubarItem.jsx b/client/components/Menubar/MenubarItem.jsx index 51aeea69ff..6f0d85b753 100644 --- a/client/components/Menubar/MenubarItem.jsx +++ b/client/components/Menubar/MenubarItem.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React, { useEffect, useContext, useRef, useMemo } from 'react'; import ButtonOrLink from '../../common/ButtonOrLink'; -import { MenubarContext, ParentMenuContext } from './contexts'; +import { MenubarContext, SubmenuContext, ParentMenuContext } from './contexts'; function MenubarItem({ id, @@ -13,13 +13,11 @@ function MenubarItem({ }) { const submenuItemRef = useRef(null); const parent = useContext(ParentMenuContext); + const { createMenuItemHandlers } = useContext(MenubarContext); - const { - createMenuItemHandlers, - registerSubmenuItem, - submenuActiveIndex, - submenuItems - } = useContext(MenubarContext); + const { registerSubmenuItem, submenuActiveIndex, submenuItems } = useContext( + SubmenuContext + ); const handlers = useMemo(() => createMenuItemHandlers(parent), [ createMenuItemHandlers, @@ -32,13 +30,13 @@ function MenubarItem({ const role = customRole || 'menuitem'; const ariaSelected = role === 'option' ? { 'aria-selected': selected } : {}; - const isActive = submenuItems[submenuActiveIndex] === id; + const isActive = submenuItems[submenuActiveIndex] === id; // is this item active in its own submenu? useEffect(() => { if (isActive && submenuItemRef.current) { submenuItemRef.current.focus(); } - }, [isActive, submenuActiveIndex]); + }, [isActive, submenuItemRef]); useEffect(() => { const unregister = registerSubmenuItem(id); diff --git a/client/components/Menubar/MenubarSubmenu.jsx b/client/components/Menubar/MenubarSubmenu.jsx index ef917d0ace..a12210c428 100644 --- a/client/components/Menubar/MenubarSubmenu.jsx +++ b/client/components/Menubar/MenubarSubmenu.jsx @@ -2,9 +2,22 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; -import React, { useEffect, useContext, useRef, useMemo } from 'react'; +import React, { + useState, + useEffect, + useCallback, + useContext, + useRef, + useMemo +} from 'react'; import TriangleIcon from '../../images/down-filled-triangle.svg'; -import { MenuOpenContext, MenubarContext, ParentMenuContext } from './contexts'; +import { + MenuOpenContext, + MenubarContext, + SubmenuContext, + ParentMenuContext +} from './contexts'; +import useKeyDownHandlers from '../../common/useKeyDownHandlers'; export function useMenuProps(id) { const activeMenu = useContext(MenuOpenContext); @@ -99,6 +112,8 @@ function MenubarSubmenu({ }) { const { isOpen, handlers } = useMenuProps(id); const { activeIndex, menuItems, registerItem } = useContext(MenubarContext); + const [submenuItems, setSubmenuItems] = useState([]); + const [submenuActiveIndex, setSubmenuActiveIndex] = useState(-1); const isActive = menuItems[activeIndex] === id; const buttonRef = useRef(null); @@ -106,6 +121,55 @@ function MenubarSubmenu({ const listRole = customListRole || 'menu'; const hasPopup = listRole === 'listbox' ? 'listbox' : 'menu'; + const keyHandlers = useMemo(() => { + // we only want to create the handlers if the menu is open, + // otherwise return empty handlers + if (!isOpen) { + return {}; + } + + return { + ArrowUp: (e) => { + e.preventDefault(); + e.stopPropagation(); + + setSubmenuActiveIndex((prev) => { + const newIndex = + (prev - 1 + submenuItems.length) % submenuItems.length; + return newIndex; + }); + }, + ArrowDown: (e) => { + e.preventDefault(); + e.stopPropagation(); + + setSubmenuActiveIndex((prev) => { + const newIndex = (prev + 1) % submenuItems.length; + return newIndex; + }); + }, + Enter: (e) => { + e.preventDefault(); + // if submenu is open, activate the focused item + // if submenu is closed, open it and focus the first item + }, + ' ': (e) => { + // same as Enter + e.preventDefault(); + }, + Escape: (e) => { + // close all submenus + }, + Tab: (e) => { + // close + } + }; + + // support direct access keys + }, [isOpen, submenuItems.length, submenuActiveIndex]); + + useKeyDownHandlers(keyHandlers); + useEffect(() => { if (isActive && buttonRef.current) { buttonRef.current.focus(); @@ -118,6 +182,33 @@ function MenubarSubmenu({ return unregister; }, [id, registerItem]); + // reset submenu active index when submenu is closed + useEffect(() => { + if (!isOpen) { + setSubmenuActiveIndex(-1); + } + }, isOpen); + + const registerSubmenuItem = useCallback((submenuId) => { + setSubmenuItems((prev) => [...prev, submenuId]); + + return () => { + setSubmenuItems((prev) => + prev.filter((currentId) => currentId !== submenuId) + ); + }; + }, []); + + const subMenuContext = useMemo( + () => ({ + submenuItems, + submenuActiveIndex, + setSubmenuActiveIndex, + registerSubmenuItem + }), + [submenuItems, submenuActiveIndex, registerSubmenuItem] + ); + return (
  • - - {children} - + + + {children} + +
  • ); } diff --git a/client/components/Menubar/contexts.jsx b/client/components/Menubar/contexts.jsx index 733fed3ae3..2ed8548a54 100644 --- a/client/components/Menubar/contexts.jsx +++ b/client/components/Menubar/contexts.jsx @@ -12,8 +12,10 @@ export const MenubarContext = createContext({ activeIndex: -1, setActiveIndex: () => {}, registerItem: () => {}, - menuItems: [], + menuItems: [] +}); +export const SubmenuContext = createContext({ // submenu state submenuActiveIndex: -1, setSubmenuActiveIndex: () => {}, From ef6a71c9ad2364e5951e4f409fb5d82d5508eda7 Mon Sep 17 00:00:00 2001 From: Tristan Espinoza Date: Fri, 17 Jan 2025 14:27:11 -0800 Subject: [PATCH 009/111] chore: code cleanup --- client/components/Menubar/Menubar.jsx | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/client/components/Menubar/Menubar.jsx b/client/components/Menubar/Menubar.jsx index 3ae2bc0b0b..d3cfdd3c41 100644 --- a/client/components/Menubar/Menubar.jsx +++ b/client/components/Menubar/Menubar.jsx @@ -41,35 +41,25 @@ function Menubar({ children, className }) { // if submenu is closed, open it and focus the last item if (menuOpen === 'none') { toggleMenuOpen(menuItems[activeIndex]); - setSubmenuActiveIndex(submenuItems.length - 1); // focus last - } else { - setSubmenuActiveIndex( - (prev) => (prev - 1 + submenuItems.length) % submenuItems.length - ); } - // if submenu is open, focus the previous item }, ArrowDown: (e) => { e.preventDefault(); // if submenu is closed, open it and focus the first item if (menuOpen === 'none') { toggleMenuOpen(menuItems[activeIndex]); - setSubmenuActiveIndex(0); // focus first - } else { - setSubmenuActiveIndex((prev) => (prev + 1) % submenuItems.length); } - // if submenu is open, focus the next item }, ArrowLeft: (e) => { e.preventDefault(); + // focus the previous item, wrapping around if we reach the beginning const newIndex = (activeIndex - 1 + menuItems.length) % menuItems.length; setActiveIndex(newIndex); - // if submenu is open, close it, open the next one and focus the next top-level item + // if submenu is open, close it if (menuOpen !== 'none') { toggleMenuOpen(menuItems[activeIndex]); - setMenuOpen(menuItems[newIndex]); } }, ArrowRight: (e) => { @@ -77,10 +67,9 @@ function Menubar({ children, className }) { const newIndex = (activeIndex + 1) % menuItems.length; setActiveIndex(newIndex); - // if submenu is open, close it, open previous one and focus the previous top-level item + // close the current submenu if it's happen if (menuOpen !== 'none') { toggleMenuOpen(menuItems[activeIndex]); - setMenuOpen(menuItems[newIndex]); } }, Enter: (e) => { From c636ccf2eac1dd3e8a4fd1effa53daeff28961d1 Mon Sep 17 00:00:00 2001 From: Tristan Espinoza Date: Fri, 17 Jan 2025 14:28:52 -0800 Subject: [PATCH 010/111] refactor: moved logo outside menubar for proper keyboard navigation --- client/modules/IDE/components/Header/Nav.jsx | 58 +++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/client/modules/IDE/components/Header/Nav.jsx b/client/modules/IDE/components/Header/Nav.jsx index 36d151ec25..afc6f3e937 100644 --- a/client/modules/IDE/components/Header/Nav.jsx +++ b/client/modules/IDE/components/Header/Nav.jsx @@ -39,6 +39,10 @@ const Nav = ({ layout }) => { ) : ( <>
    +
    + +
    + @@ -87,19 +91,40 @@ const UserMenu = () => { return null; }; -const DashboardMenu = () => { +const Logo = () => { const { t } = useTranslation(); - const editorLink = useSelector(selectSketchPath); - return ( -
      -
    • + const user = useSelector((state) => state.user); + + if (user?.username) { + return ( + -
    • + + ); + } + + return ( +
      + + + ); +}; + +const DashboardMenu = () => { + const { t } = useTranslation(); + const editorLink = useSelector(selectSketchPath); + return ( +
      • { return (
          -
        • - {user && user.username !== undefined ? ( - - - - ) : ( - - - - )} -
        • {t('Nav.File.New')} From 84202cea339d75b4cd4abec7c9c59400315668dd Mon Sep 17 00:00:00 2001 From: Tristan Espinoza Date: Fri, 17 Jan 2025 14:49:44 -0800 Subject: [PATCH 011/111] fix: fixed inconsistent hook rendering in MenubarSubmenu --- client/components/Menubar/MenubarSubmenu.jsx | 39 +++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/client/components/Menubar/MenubarSubmenu.jsx b/client/components/Menubar/MenubarSubmenu.jsx index a12210c428..5ab200c87c 100644 --- a/client/components/Menubar/MenubarSubmenu.jsx +++ b/client/components/Menubar/MenubarSubmenu.jsx @@ -114,47 +114,42 @@ function MenubarSubmenu({ const { activeIndex, menuItems, registerItem } = useContext(MenubarContext); const [submenuItems, setSubmenuItems] = useState([]); const [submenuActiveIndex, setSubmenuActiveIndex] = useState(-1); - const isActive = menuItems[activeIndex] === id; const buttonRef = useRef(null); + const isActive = menuItems[activeIndex] === id; const triggerRole = customTriggerRole || 'menuitem'; const listRole = customListRole || 'menu'; const hasPopup = listRole === 'listbox' ? 'listbox' : 'menu'; - const keyHandlers = useMemo(() => { - // we only want to create the handlers if the menu is open, - // otherwise return empty handlers - if (!isOpen) { - return {}; - } - - return { + const keyHandlers = useMemo( + () => ({ + // we only want to create the handlers if the menu is open, + // otherwise early return{ ArrowUp: (e) => { + if (!isOpen) return; e.preventDefault(); e.stopPropagation(); - setSubmenuActiveIndex((prev) => { - const newIndex = - (prev - 1 + submenuItems.length) % submenuItems.length; - return newIndex; - }); + setSubmenuActiveIndex( + (prev) => (prev - 1 + submenuItems.length) % submenuItems.length + ); }, ArrowDown: (e) => { + if (!isOpen) return; e.preventDefault(); e.stopPropagation(); - setSubmenuActiveIndex((prev) => { - const newIndex = (prev + 1) % submenuItems.length; - return newIndex; - }); + setSubmenuActiveIndex((prev) => (prev + 1) % submenuItems.length); }, Enter: (e) => { + if (!isOpen) return; e.preventDefault(); // if submenu is open, activate the focused item // if submenu is closed, open it and focus the first item }, ' ': (e) => { // same as Enter + if (!isOpen) return; e.preventDefault(); }, Escape: (e) => { @@ -163,10 +158,10 @@ function MenubarSubmenu({ Tab: (e) => { // close } - }; - - // support direct access keys - }, [isOpen, submenuItems.length, submenuActiveIndex]); + // support direct access keys + }), + [isOpen, submenuItems.length, submenuActiveIndex] + ); useKeyDownHandlers(keyHandlers); From 9fb7b9eabdf4a7e67e583e1b18b9f6dbe2620e49 Mon Sep 17 00:00:00 2001 From: Tristan Espinoza Date: Fri, 17 Jan 2025 15:07:21 -0800 Subject: [PATCH 012/111] fix: fixed useEffect getting a boolean instead of dependency array --- client/components/Menubar/MenubarSubmenu.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/Menubar/MenubarSubmenu.jsx b/client/components/Menubar/MenubarSubmenu.jsx index 5ab200c87c..92c0402c38 100644 --- a/client/components/Menubar/MenubarSubmenu.jsx +++ b/client/components/Menubar/MenubarSubmenu.jsx @@ -182,7 +182,7 @@ function MenubarSubmenu({ if (!isOpen) { setSubmenuActiveIndex(-1); } - }, isOpen); + }, [isOpen]); const registerSubmenuItem = useCallback((submenuId) => { setSubmenuItems((prev) => [...prev, submenuId]); From ce789d71b0399898a509bdddb957fcfd1d91b1cf Mon Sep 17 00:00:00 2001 From: Tristan Espinoza Date: Sat, 18 Jan 2025 14:50:56 -0800 Subject: [PATCH 013/111] refactor: renamed variables in preparation of state management refactor --- client/components/Menubar/MenubarSubmenu.jsx | 44 ++++++++++---------- client/components/Menubar/contexts.jsx | 16 +++---- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/client/components/Menubar/MenubarSubmenu.jsx b/client/components/Menubar/MenubarSubmenu.jsx index 92c0402c38..3fbe40b313 100644 --- a/client/components/Menubar/MenubarSubmenu.jsx +++ b/client/components/Menubar/MenubarSubmenu.jsx @@ -111,12 +111,14 @@ function MenubarSubmenu({ ...props }) { const { isOpen, handlers } = useMenuProps(id); - const { activeIndex, menuItems, registerItem } = useContext(MenubarContext); - const [submenuItems, setSubmenuItems] = useState([]); - const [submenuActiveIndex, setSubmenuActiveIndex] = useState(-1); + const { oldActiveIndex, oldMenuItems, oldRegisterItem } = useContext( + MenubarContext + ); + const [oldSubmenuItems, setOldSubmenuItems] = useState([]); + const [oldSubmenuActiveIndex, setOldSubmenuActiveIndex] = useState(-1); const buttonRef = useRef(null); - const isActive = menuItems[activeIndex] === id; + const isActive = oldMenuItems[oldActiveIndex] === id; const triggerRole = customTriggerRole || 'menuitem'; const listRole = customListRole || 'menu'; const hasPopup = listRole === 'listbox' ? 'listbox' : 'menu'; @@ -130,8 +132,8 @@ function MenubarSubmenu({ e.preventDefault(); e.stopPropagation(); - setSubmenuActiveIndex( - (prev) => (prev - 1 + submenuItems.length) % submenuItems.length + setOldSubmenuActiveIndex( + (prev) => (prev - 1 + oldSubmenuItems.length) % oldSubmenuItems.length ); }, ArrowDown: (e) => { @@ -139,7 +141,7 @@ function MenubarSubmenu({ e.preventDefault(); e.stopPropagation(); - setSubmenuActiveIndex((prev) => (prev + 1) % submenuItems.length); + setOldSubmenuActiveIndex((prev) => (prev + 1) % oldSubmenuItems.length); }, Enter: (e) => { if (!isOpen) return; @@ -160,7 +162,7 @@ function MenubarSubmenu({ } // support direct access keys }), - [isOpen, submenuItems.length, submenuActiveIndex] + [isOpen, oldSubmenuItems.length, oldSubmenuActiveIndex] ); useKeyDownHandlers(keyHandlers); @@ -173,35 +175,35 @@ function MenubarSubmenu({ // register this menu item useEffect(() => { - const unregister = registerItem(id); + const unregister = oldRegisterItem(id); return unregister; - }, [id, registerItem]); + }, [id, oldRegisterItem]); // reset submenu active index when submenu is closed useEffect(() => { if (!isOpen) { - setSubmenuActiveIndex(-1); + setOldSubmenuActiveIndex(-1); } }, [isOpen]); - const registerSubmenuItem = useCallback((submenuId) => { - setSubmenuItems((prev) => [...prev, submenuId]); + const oldRegisterSubmenuItem = useCallback((submenuId) => { + setOldSubmenuItems((prev) => [...prev, submenuId]); return () => { - setSubmenuItems((prev) => + setOldSubmenuItems((prev) => prev.filter((currentId) => currentId !== submenuId) ); }; }, []); - const subMenuContext = useMemo( + const submenuContext = useMemo( () => ({ - submenuItems, - submenuActiveIndex, - setSubmenuActiveIndex, - registerSubmenuItem + oldSubmenuItems, + oldSubmenuActiveIndex, + setOldSubmenuActiveIndex, + oldRegisterSubmenuItem }), - [submenuItems, submenuActiveIndex, registerSubmenuItem] + [oldSubmenuItems, oldSubmenuActiveIndex, oldRegisterSubmenuItem] ); return ( @@ -216,7 +218,7 @@ function MenubarSubmenu({ {...handlers} {...props} /> - + {children} diff --git a/client/components/Menubar/contexts.jsx b/client/components/Menubar/contexts.jsx index 2ed8548a54..ae39308674 100644 --- a/client/components/Menubar/contexts.jsx +++ b/client/components/Menubar/contexts.jsx @@ -9,16 +9,16 @@ export const MenubarContext = createContext({ createMenuHandlers: () => ({}), createMenuItemHandlers: () => ({}), toggleMenuOpen: () => {}, - activeIndex: -1, - setActiveIndex: () => {}, - registerItem: () => {}, - menuItems: [] + oldActiveIndex: -1, + setOldActiveIndex: () => {}, + oldRegisterItem: () => {}, + oldMenuItems: [] }); export const SubmenuContext = createContext({ // submenu state - submenuActiveIndex: -1, - setSubmenuActiveIndex: () => {}, - registerSubmenuItem: () => {}, - submenuItems: [] + oldSubmenuActiveIndex: -1, + setOldSubmenuActiveIndex: () => {}, + oldRegisterSubmenuItem: () => {}, + oldSubmenuItems: [] }); From 63a6444f1cadb85c8cab2b413937d8187992ff1a Mon Sep 17 00:00:00 2001 From: Tristan Espinoza Date: Sat, 18 Jan 2025 14:51:42 -0800 Subject: [PATCH 014/111] feat: added usePrevious hook for obtaining prev state value --- client/common/usePrevious.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 client/common/usePrevious.js diff --git a/client/common/usePrevious.js b/client/common/usePrevious.js new file mode 100644 index 0000000000..0f79ed91a6 --- /dev/null +++ b/client/common/usePrevious.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export default function usePrevious(value) { + const ref = React.useRef(); + + React.useEffect(() => { + ref.current = value; + }, [value]); + + return ref.current; +} From dc62a3b81b329fe65e5e60151be1d29f8f21a182 Mon Sep 17 00:00:00 2001 From: Tristan Espinoza Date: Sat, 18 Jan 2025 14:52:10 -0800 Subject: [PATCH 015/111] refactor: migrated menu item collections from Array to Set --- client/components/Menubar/Menubar.jsx | 97 +++++++++++++++-------- client/components/Menubar/MenubarItem.jsx | 54 ++++++++----- 2 files changed, 98 insertions(+), 53 deletions(-) diff --git a/client/components/Menubar/Menubar.jsx b/client/components/Menubar/Menubar.jsx index d3cfdd3c41..d77a2788de 100644 --- a/client/components/Menubar/Menubar.jsx +++ b/client/components/Menubar/Menubar.jsx @@ -1,29 +1,42 @@ import PropTypes from 'prop-types'; -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { + useCallback, + useMemo, + useRef, + useState, + useEffect +} from 'react'; import useModalClose from '../../common/useModalClose'; import { MenuOpenContext, MenubarContext } from './contexts'; import useKeyDownHandlers from '../../common/useKeyDownHandlers'; +import usePrevious from '../../common/usePrevious'; function Menubar({ children, className }) { const [menuOpen, setMenuOpen] = useState('none'); - const [activeIndex, setActiveIndex] = useState(-1); - const [menuItems, setMenuItems] = useState([]); - const [submenuActiveIndex, setSubmenuActiveIndex] = useState(-1); - const [submenuItems, setSubmenuItems] = useState([]); + + const menuItems = useRef(new Set()).current; + const [activeIndex, setActiveIndex] = useState(0); + const prevIndex = usePrevious(activeIndex); + + // old state variables + const [oldActiveIndex, setOldActiveIndex] = useState(-1); + const [oldMenuItems, setOldMenuItems] = useState([]); + const [oldSubmenuActiveIndex, setOldSubmenuActiveIndex] = useState(-1); + const [oldSubmenuItems, setOldSubmenuItems] = useState([]); const timerRef = useRef(null); const nodeRef = useRef(null); - const registerItem = useCallback((id) => { - setMenuItems((prev) => [...prev, id]); + const oldRegisterItem = useCallback((id) => { + setOldMenuItems((prev) => [...prev, id]); return () => { - setMenuItems((prev) => prev.filter((item) => item !== id)); + setOldMenuItems((prev) => prev.filter((item) => item !== id)); }; }, []); - const registerSubmenuItem = useCallback((id) => { - setSubmenuItems((prev) => [...prev, id]); + const oldRegisterSubmenuItem = useCallback((id) => { + setOldSubmenuItems((prev) => [...prev, id]); return () => { - setSubmenuItems((prev) => prev.filter((item) => item !== id)); + setOldSubmenuItems((prev) => prev.filter((item) => item !== id)); }; }, []); @@ -40,50 +53,50 @@ function Menubar({ children, className }) { e.preventDefault(); // if submenu is closed, open it and focus the last item if (menuOpen === 'none') { - toggleMenuOpen(menuItems[activeIndex]); + toggleMenuOpen(oldMenuItems[oldActiveIndex]); } }, ArrowDown: (e) => { e.preventDefault(); // if submenu is closed, open it and focus the first item if (menuOpen === 'none') { - toggleMenuOpen(menuItems[activeIndex]); + toggleMenuOpen(oldMenuItems[oldActiveIndex]); } }, ArrowLeft: (e) => { e.preventDefault(); // focus the previous item, wrapping around if we reach the beginning const newIndex = - (activeIndex - 1 + menuItems.length) % menuItems.length; - setActiveIndex(newIndex); + (oldActiveIndex - 1 + oldMenuItems.length) % oldMenuItems.length; + setOldActiveIndex(newIndex); // if submenu is open, close it if (menuOpen !== 'none') { - toggleMenuOpen(menuItems[activeIndex]); + toggleMenuOpen(oldMenuItems[oldActiveIndex]); } }, ArrowRight: (e) => { e.preventDefault(); - const newIndex = (activeIndex + 1) % menuItems.length; - setActiveIndex(newIndex); + const newIndex = (oldActiveIndex + 1) % oldMenuItems.length; + setOldActiveIndex(newIndex); // close the current submenu if it's happen if (menuOpen !== 'none') { - toggleMenuOpen(menuItems[activeIndex]); + toggleMenuOpen(oldMenuItems[oldActiveIndex]); } }, Enter: (e) => { e.preventDefault(); // if submenu is open, activate the focused item // if submenu is closed, open it and focus the first item - toggleMenuOpen(menuItems[activeIndex]); + toggleMenuOpen(oldMenuItems[oldActiveIndex]); }, ' ': (e) => { // same as Enter e.preventDefault(); // if submenu is open, activate the focused item // if submenu is closed, open it and focus the first item - toggleMenuOpen(menuItems[activeIndex]); + toggleMenuOpen(oldMenuItems[oldActiveIndex]); }, Escape: (e) => { // close all submenus @@ -94,7 +107,7 @@ function Menubar({ children, className }) { } // support direct access keys }), - [menuItems, menuOpen, activeIndex, toggleMenuOpen] + [oldMenuItems, menuOpen, oldActiveIndex, toggleMenuOpen] ); useKeyDownHandlers(keyHandlers); @@ -113,10 +126,22 @@ function Menubar({ children, className }) { const handleBlur = useCallback(() => { timerRef.current = setTimeout(() => { setMenuOpen('none'); - setActiveIndex(-1); + setOldActiveIndex(-1); }, 10); }, [timerRef, setMenuOpen]); + useEffect(() => { + if (activeIndex !== prevIndex) { + const items = Array.from(menuItems); + const activeNode = items[activeIndex]?.firstChild; + const prevNode = items[prevIndex]?.firstChild; + + prevNode?.setAttribute('tabindex', '-1'); + activeNode?.setAttribute('tabindex', '0'); + activeNode.focus(); + } + }, [activeIndex, prevIndex, menuItems]); + const contextValue = useMemo( () => ({ createMenuHandlers: (menu) => ({ @@ -143,26 +168,28 @@ function Menubar({ children, className }) { } }), toggleMenuOpen, - activeIndex, - setActiveIndex, - registerItem, menuItems, - submenuActiveIndex, - setSubmenuActiveIndex, - registerSubmenuItem, - submenuItems + oldActiveIndex, + setOldActiveIndex, + oldRegisterItem, + oldMenuItems, + oldSubmenuActiveIndex, + setOldSubmenuActiveIndex, + oldRegisterSubmenuItem, + oldSubmenuItems }), [ menuOpen, toggleMenuOpen, clearHideTimeout, handleBlur, - activeIndex, - registerItem, menuItems, - submenuActiveIndex, - registerSubmenuItem, - submenuItems + oldActiveIndex, + oldRegisterItem, + oldMenuItems, + oldSubmenuActiveIndex, + oldRegisterSubmenuItem, + oldSubmenuItems ] ); diff --git a/client/components/Menubar/MenubarItem.jsx b/client/components/Menubar/MenubarItem.jsx index 6f0d85b753..59ad968f78 100644 --- a/client/components/Menubar/MenubarItem.jsx +++ b/client/components/Menubar/MenubarItem.jsx @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React, { useEffect, useContext, useRef, useMemo } from 'react'; +import React, { useState, useEffect, useContext, useRef, useMemo } from 'react'; import ButtonOrLink from '../../common/ButtonOrLink'; import { MenubarContext, SubmenuContext, ParentMenuContext } from './contexts'; @@ -11,13 +11,17 @@ function MenubarItem({ selected, ...rest }) { - const submenuItemRef = useRef(null); + const [isFirstChild, setIsFirstChild] = useState(false); + const { createMenuItemHandlers, menuItems } = useContext(MenubarContext); + const oldSubmenuItemRef = useRef(null); + const menuItemRef = useRef(null); const parent = useContext(ParentMenuContext); - const { createMenuItemHandlers } = useContext(MenubarContext); - const { registerSubmenuItem, submenuActiveIndex, submenuItems } = useContext( - SubmenuContext - ); + const { + oldRegisterSubmenuItem, + oldSubmenuActiveIndex, + oldSubmenuItems + } = useContext(SubmenuContext); const handlers = useMemo(() => createMenuItemHandlers(parent), [ createMenuItemHandlers, @@ -30,18 +34,32 @@ function MenubarItem({ const role = customRole || 'menuitem'; const ariaSelected = role === 'option' ? { 'aria-selected': selected } : {}; - const isActive = submenuItems[submenuActiveIndex] === id; // is this item active in its own submenu? + const isActive = oldSubmenuItems[oldSubmenuActiveIndex] === id; // is this item active in its own submenu? useEffect(() => { - if (isActive && submenuItemRef.current) { - submenuItemRef.current.focus(); + if (isActive && oldSubmenuItemRef.current) { + oldSubmenuItemRef.current.focus(); } - }, [isActive, submenuItemRef]); + }, [isActive, oldSubmenuItemRef]); + + useEffect(() => { + const menuItemNode = menuItemRef.current; + if (menuItemNode) { + if (menuItems.size === 0) { + setIsFirstChild(true); + } + menuItems.add(menuItemNode); + } + + return () => { + menuItems.delete(menuItemNode); + }; + }, [menuItems]); useEffect(() => { - const unregister = registerSubmenuItem(id); + const unregister = oldRegisterSubmenuItem(id); return unregister; - }, [id, registerSubmenuItem]); + }, [id, oldRegisterSubmenuItem]); useEffect(() => { if (isActive) { @@ -49,21 +67,21 @@ function MenubarItem({ id, isActive, parent, - submenuActiveIndex, - element: submenuItemRef.current + oldSubmenuActiveIndex, + element: oldSubmenuItemRef.current }); } - }, [isActive, id, parent, submenuActiveIndex]); + }, [isActive, id, parent, oldSubmenuActiveIndex]); return ( -
        • +
        • From d3ccf166b3db0ef6476eb60e2eb632ab7716748c Mon Sep 17 00:00:00 2001 From: Tristan Espinoza Date: Tue, 21 Jan 2025 17:04:17 -0800 Subject: [PATCH 016/111] feat: added usePrevious hook for focus management --- client/modules/IDE/hooks/usePrevious.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 client/modules/IDE/hooks/usePrevious.js diff --git a/client/modules/IDE/hooks/usePrevious.js b/client/modules/IDE/hooks/usePrevious.js new file mode 100644 index 0000000000..ae050ae1e1 --- /dev/null +++ b/client/modules/IDE/hooks/usePrevious.js @@ -0,0 +1,9 @@ +import React, { useEffect, useRef } from 'react'; + +export default function usePrevious(value) { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }, [value]); + return ref.current; +} From f3555dc7f2eaa4badac67eac8ed63ac48e968d53 Mon Sep 17 00:00:00 2001 From: Tristan Espinoza Date: Tue, 21 Jan 2025 17:05:38 -0800 Subject: [PATCH 017/111] refactor: migrated collections tracking for top level components from array to set --- client/components/Menubar/Menubar.jsx | 59 +++++++++++++--- client/components/Menubar/MenubarSubmenu.jsx | 72 +++++++++++++++++++- 2 files changed, 122 insertions(+), 9 deletions(-) diff --git a/client/components/Menubar/Menubar.jsx b/client/components/Menubar/Menubar.jsx index d77a2788de..3135763fef 100644 --- a/client/components/Menubar/Menubar.jsx +++ b/client/components/Menubar/Menubar.jsx @@ -17,6 +17,7 @@ function Menubar({ children, className }) { const menuItems = useRef(new Set()).current; const [activeIndex, setActiveIndex] = useState(0); const prevIndex = usePrevious(activeIndex); + const [isFirstChild, setIsFirstChild] = useState(false); // old state variables const [oldActiveIndex, setOldActiveIndex] = useState(-1); @@ -40,6 +41,25 @@ function Menubar({ children, className }) { }; }, []); + const registerTopLevelItem = useCallback( + (ref) => { + const menuItemNode = ref.current; + + if (menuItemNode) { + if (menuItems.size === 0) { + setIsFirstChild(true); + } + menuItems.add(menuItemNode); + console.log('Menubar Register: ', menuItemNode.textContent); + } + + return () => { + menuItems.delete(menuItemNode); + }; + }, + [menuItems] + ); + const toggleMenuOpen = useCallback( (menu) => { setMenuOpen((prevState) => (prevState === menu ? 'none' : menu)); @@ -66,9 +86,12 @@ function Menubar({ children, className }) { ArrowLeft: (e) => { e.preventDefault(); // focus the previous item, wrapping around if we reach the beginning - const newIndex = - (oldActiveIndex - 1 + oldMenuItems.length) % oldMenuItems.length; - setOldActiveIndex(newIndex); + // const newIndex = + // (oldActiveIndex - 1 + oldMenuItems.length) % oldMenuItems.length; + // setOldActiveIndex(newIndex); + + const newIndex = (activeIndex - 1 + menuItems.size) % menuItems.size; + setActiveIndex(newIndex); // if submenu is open, close it if (menuOpen !== 'none') { @@ -77,8 +100,12 @@ function Menubar({ children, className }) { }, ArrowRight: (e) => { e.preventDefault(); - const newIndex = (oldActiveIndex + 1) % oldMenuItems.length; - setOldActiveIndex(newIndex); + // const newIndex = (oldActiveIndex + 1) % oldMenuItems.length; + // setOldActiveIndex(newIndex); + + const newIndex = (activeIndex + 1) % menuItems.size; + console.log(activeIndex, newIndex, menuItems.size); + setActiveIndex(newIndex); // close the current submenu if it's happen if (menuOpen !== 'none') { @@ -107,7 +134,14 @@ function Menubar({ children, className }) { } // support direct access keys }), - [oldMenuItems, menuOpen, oldActiveIndex, toggleMenuOpen] + [ + menuItems, + activeIndex, + oldMenuItems, + menuOpen, + oldActiveIndex, + toggleMenuOpen + ] ); useKeyDownHandlers(keyHandlers); @@ -133,8 +167,9 @@ function Menubar({ children, className }) { useEffect(() => { if (activeIndex !== prevIndex) { const items = Array.from(menuItems); - const activeNode = items[activeIndex]?.firstChild; - const prevNode = items[prevIndex]?.firstChild; + const activeNode = items[activeIndex]; + const prevNode = items[prevIndex]; + console.log(activeNode, prevNode); prevNode?.setAttribute('tabindex', '-1'); activeNode?.setAttribute('tabindex', '0'); @@ -169,6 +204,10 @@ function Menubar({ children, className }) { }), toggleMenuOpen, menuItems, + activeIndex, + setActiveIndex, + registerTopLevelItem, + isFirstChild, oldActiveIndex, setOldActiveIndex, oldRegisterItem, @@ -184,6 +223,10 @@ function Menubar({ children, className }) { clearHideTimeout, handleBlur, menuItems, + activeIndex, + setActiveIndex, + registerTopLevelItem, + isFirstChild, oldActiveIndex, oldRegisterItem, oldMenuItems, diff --git a/client/components/Menubar/MenubarSubmenu.jsx b/client/components/Menubar/MenubarSubmenu.jsx index 3fbe40b313..592ebc98aa 100644 --- a/client/components/Menubar/MenubarSubmenu.jsx +++ b/client/components/Menubar/MenubarSubmenu.jsx @@ -40,6 +40,38 @@ export function useMenuProps(id) { const MenubarTrigger = React.forwardRef( ({ id, title, role, hasPopup, ...props }, ref) => { const { isOpen, handlers } = useMenuProps(id); + const { + menuItems, + activeIndex, + registerTopLevelItem, + isFirstChild + } = useContext(MenubarContext); + + // const isActive = menuItems[activeIndex] === id; // is this item active in its own submenu? + const isActive = useMemo(() => { + const items = Array.from(menuItems); + const activeNode = items[activeIndex]; + // console.log(`${items[activeIndex]?.id}, ${id}`); + console.log(`${activeNode}, ${ref.current}`); + return items[activeIndex]?.id === id; + }, [menuItems, activeIndex, id]); + + useEffect(() => { + const unregister = registerTopLevelItem(ref); + return unregister; + }, [menuItems, registerTopLevelItem]); + + useEffect(() => { + // oldSubmenuItemRef.current.focus(); + const items = Array.from(menuItems); + console.log( + `${items[activeIndex]}: ${isActive}, index: ${activeIndex}, ref: ${ref.current}, id: ${id}` + ); + + if (isActive && ref.current) { + ref.current.focus(); + } + }, [ref, isActive, activeIndex]); return (
        ); } @@ -227,7 +227,7 @@ Menubar.propTypes = { Menubar.defaultProps = { children: null, - className: 'nav' + className: 'nav__menubar' }; export default Menubar; diff --git a/client/modules/IDE/components/Header/Nav.jsx b/client/modules/IDE/components/Header/Nav.jsx index afc6f3e937..ebb7e6e9b7 100644 --- a/client/modules/IDE/components/Header/Nav.jsx +++ b/client/modules/IDE/components/Header/Nav.jsx @@ -43,10 +43,12 @@ const Nav = ({ layout }) => { - - - - +
    ); @@ -124,7 +126,7 @@ const DashboardMenu = () => { const { t } = useTranslation(); const editorLink = useSelector(selectSketchPath); return ( - `; From cb8001f722d0f1da263d1d7ee8fb0f9ec12d8be4 Mon Sep 17 00:00:00 2001 From: Tristan Espinoza Date: Fri, 31 Jan 2025 22:36:49 -0800 Subject: [PATCH 078/111] chore: fixed lint warnings and small optimizations --- client/common/usePrevious.js | 6 +- client/components/Menubar/Menubar.jsx | 60 ++-- client/components/Menubar/Menubar.test.jsx | 1 - client/components/Menubar/MenubarItem.jsx | 10 +- client/components/Menubar/MenubarSubmenu.jsx | 276 +++++++++---------- client/components/Menubar/contexts.jsx | 1 - client/modules/IDE/components/Header/Nav.jsx | 2 +- client/modules/IDE/hooks/usePrevious.js | 9 - 8 files changed, 167 insertions(+), 198 deletions(-) delete mode 100644 client/modules/IDE/hooks/usePrevious.js diff --git a/client/common/usePrevious.js b/client/common/usePrevious.js index 0f79ed91a6..ed46581cb0 100644 --- a/client/common/usePrevious.js +++ b/client/common/usePrevious.js @@ -1,9 +1,9 @@ -import React from 'react'; +import { useEffect, useRef } from 'react'; export default function usePrevious(value) { - const ref = React.useRef(); + const ref = useRef(); - React.useEffect(() => { + useEffect(() => { ref.current = value; }, [value]); diff --git a/client/components/Menubar/Menubar.jsx b/client/components/Menubar/Menubar.jsx index b87b365e64..439aa0768d 100644 --- a/client/components/Menubar/Menubar.jsx +++ b/client/components/Menubar/Menubar.jsx @@ -36,7 +36,6 @@ function Menubar({ children, className }) { const [activeIndex, setActiveIndex] = useState(0); const prevIndex = usePrevious(activeIndex); const [hasFocus, setHasFocus] = useState(false); - const [isFirstChild, setIsFirstChild] = useState(false); // refs for menu items and their ids const menuItems = useRef(new Set()).current; @@ -161,31 +160,38 @@ function Menubar({ children, className }) { }, [nodeRef]); // keyboard navigation - const keyHandlers = useMemo( - () => ({ - ArrowLeft: (e) => { - e.preventDefault(); - e.stopPropagation(); - prev(); - }, - ArrowRight: (e) => { - e.preventDefault(); - e.stopPropagation(); - next(); - }, - Escape: (e) => { - e.preventDefault(); - e.stopPropagation(); - close(); - }, - Tab: (e) => { - e.stopPropagation(); - // close - } - // to do: support direct access keys - }), - [menuItems, activeIndex, menuOpen] - ); + const keyHandlers = { + ArrowLeft: (e) => { + e.preventDefault(); + e.stopPropagation(); + prev(); + }, + ArrowRight: (e) => { + e.preventDefault(); + e.stopPropagation(); + next(); + }, + Escape: (e) => { + e.preventDefault(); + e.stopPropagation(); + close(); + }, + Tab: (e) => { + e.stopPropagation(); + // close + }, + Home: (e) => { + e.preventDefault(); + e.stopPropagation(); + first(); + }, + End: (e) => { + e.preventDefault(); + e.stopPropagation(); + last(); + } + // to do: support direct access keys + }; // focus the active menu item and set its tabindex useEffect(() => { @@ -242,7 +248,6 @@ function Menubar({ children, className }) { registerTopLevelItem, setMenuOpen, toggleMenuOpen, - isFirstChild, hasFocus, setHasFocus }), @@ -253,7 +258,6 @@ function Menubar({ children, className }) { registerTopLevelItem, menuOpen, toggleMenuOpen, - isFirstChild, hasFocus, setHasFocus, clearHideTimeout, diff --git a/client/components/Menubar/Menubar.test.jsx b/client/components/Menubar/Menubar.test.jsx index 294eaf712d..0f78d2f547 100644 --- a/client/components/Menubar/Menubar.test.jsx +++ b/client/components/Menubar/Menubar.test.jsx @@ -89,7 +89,6 @@ describe('Menubar', () => { const openMenuItem = screen.getByRole('menuitem', { name: 'Open' }); const editMenuTrigger = screen.getByRole('menuitem', { name: 'Edit' }); - const tidyMenuItem = screen.getByRole('menuitem', { name: 'Tidy' }); fireEvent.click(fileMenuTrigger); expect(fileMenuTrigger).toHaveAttribute('aria-expanded', 'true'); diff --git a/client/components/Menubar/MenubarItem.jsx b/client/components/Menubar/MenubarItem.jsx index 8b314025ae..be0ccacdc7 100644 --- a/client/components/Menubar/MenubarItem.jsx +++ b/client/components/Menubar/MenubarItem.jsx @@ -58,8 +58,7 @@ function MenubarItem({ const { setSubmenuActiveIndex, submenuItems, - registerSubmenuItem, - isFirstChild + registerSubmenuItem } = useContext(SubmenuContext); const parent = useContext(ParentMenuContext); @@ -67,10 +66,7 @@ function MenubarItem({ const menuItemRef = useRef(null); // handlers from parent menu - const handlers = useMemo(() => createMenuItemHandlers(parent), [ - createMenuItemHandlers, - parent - ]); + const handlers = createMenuItemHandlers(parent); // role and aria-selected const role = customRole || 'menuitem'; @@ -100,7 +96,7 @@ function MenubarItem({ {...handlers} {...ariaSelected} role={role} - tabIndex={isFirstChild ? 0 : -1} + tabIndex={-1} id={id} /> diff --git a/client/components/Menubar/MenubarSubmenu.jsx b/client/components/Menubar/MenubarSubmenu.jsx index e91d38b231..37f9fbc57c 100644 --- a/client/components/Menubar/MenubarSubmenu.jsx +++ b/client/components/Menubar/MenubarSubmenu.jsx @@ -40,8 +40,6 @@ export function useMenuProps(id) { /** * @component * @param {Object} props - * @param {string} props.id - The id of submenu the trigger button controls - * @param {string} props.title - The title of the trigger button * @param {string} [props.role='menuitem'] - The ARIA role of the trigger button * @param {string} [props.hasPopup='menu'] - The ARIA property that indicates the presence of a popup * @returns {JSX.Element} @@ -58,8 +56,6 @@ export function useMenuProps(id) { * > * */ -const MenubarTrigger = React.forwardRef( - ({ id, role, title, hasPopup, ...props }, ref) => { - const { isOpen, handlers } = useMenuProps(id); - const { - setActiveIndex, - menuItems, - registerTopLevelItem, - hasFocus, - toggleMenuOpen - } = useContext(MenubarContext); - const { first, last } = useContext(SubmenuContext); - - // update active index when mouse enters the trigger and the menu has focus - const handleMouseEnter = () => { - if (hasFocus) { - const items = Array.from(menuItems); - const index = items.findIndex((item) => item === ref.current); - - if (index !== -1) { - setActiveIndex(index); - } - } - }; +const MenubarTrigger = React.forwardRef(({ role, hasPopup, ...props }, ref) => { + const { + setActiveIndex, + menuItems, + registerTopLevelItem, + hasFocus + } = useContext(MenubarContext); + const { id, title, first, last } = useContext(SubmenuContext); + const { isOpen, handlers } = useMenuProps(id); + + // update active index when mouse enters the trigger and the menu has focus + const handleMouseEnter = () => { + if (hasFocus) { + const items = Array.from(menuItems); + const index = items.findIndex((item) => item === ref.current); - // keyboard handlers - const handleKeyDown = (e) => { - switch (e.key) { - case 'ArrowDown': - if (!isOpen) { - e.preventDefault(); - e.stopPropagation(); - first(); - } - break; - case 'ArrowUp': - if (!isOpen) { - e.preventDefault(); - e.stopPropagation(); - last(); - } - break; - case 'Enter': - case ' ': - if (!isOpen) { - e.preventDefault(); - e.stopPropagation(); - first(); - } - break; - default: - break; + if (index !== -1) { + setActiveIndex(index); } - }; + } + }; + + // keyboard handlers + const handleKeyDown = (e) => { + switch (e.key) { + case 'ArrowDown': + if (!isOpen) { + e.preventDefault(); + e.stopPropagation(); + first(); + } + break; + case 'ArrowUp': + if (!isOpen) { + e.preventDefault(); + e.stopPropagation(); + last(); + } + break; + case 'Enter': + case ' ': + if (!isOpen) { + e.preventDefault(); + e.stopPropagation(); + first(); + } + break; + default: + break; + } + }; - // register trigger with parent menubar - useEffect(() => { - const unregister = registerTopLevelItem(ref, id); - return unregister; - }, [menuItems, registerTopLevelItem]); - - return ( - - ); - } -); + // register trigger with parent menubar + useEffect(() => { + const unregister = registerTopLevelItem(ref, id); + return unregister; + }, [menuItems, registerTopLevelItem]); + + return ( + + ); +}); MenubarTrigger.propTypes = { - id: PropTypes.string.isRequired, role: PropTypes.string, - title: PropTypes.node.isRequired, hasPopup: PropTypes.oneOf(['menu', 'listbox', 'true']) }; @@ -172,9 +163,7 @@ MenubarTrigger.defaultProps = { * @component * @param {Object} props * @param {React.ReactNode} props.children - MenubarItems that should be rendered in the list - * @param {string} props.id - The unique id of the submenu * @param {string} [props.role='menu'] - The ARIA role of the list element - * @param {string} props.title - The title of the list element * @returns {JSX.Element} */ @@ -182,12 +171,14 @@ MenubarTrigger.defaultProps = { * MenubarList renders the container for menu items in a submenu. It provides context and handles ARIA roles. * * @example - * + * * ... elements * */ -function MenubarList({ children, id, role, title, ...props }) { +function MenubarList({ children, role, ...props }) { + const { id, title } = useContext(SubmenuContext); + return (
  • Date: Wed, 16 Apr 2025 12:12:29 -0700 Subject: [PATCH 089/111] fix: potential fix for blur updating state after unmount --- client/components/Menubar/Menubar.jsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/components/Menubar/Menubar.jsx b/client/components/Menubar/Menubar.jsx index 5b0d4cbd4f..8d7ff38da0 100644 --- a/client/components/Menubar/Menubar.jsx +++ b/client/components/Menubar/Menubar.jsx @@ -154,8 +154,10 @@ function Menubar({ children, className }) { if (!isInMenu) { timerRef.current = setTimeout(() => { - setMenuOpen('none'); - setHasFocus(false); + if (nodeRef.current) { + setMenuOpen('none'); + setHasFocus(false); + } }, 10); } }, @@ -213,6 +215,10 @@ function Menubar({ children, className }) { } }, [activeIndex, prevIndex, menuItems]); + useEffect(() => { + clearHideTimeout(); + }, [clearHideTimeout]); + // context value for dropdowns and menu items const contextValue = useMemo( () => ({ From d1d1e024e210d569d57d4ad339c743ce789ccf7b Mon Sep 17 00:00:00 2001 From: Tristan Espinoza Date: Thu, 1 May 2025 13:59:05 -0700 Subject: [PATCH 090/111] fix: applied a temp fix for errors related to using menuitems on mobile --- .../IDE/components/Header/MobileNav.jsx | 74 +++++++++++-------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/client/modules/IDE/components/Header/MobileNav.jsx b/client/modules/IDE/components/Header/MobileNav.jsx index 349d3d1709..f792dd158f 100644 --- a/client/modules/IDE/components/Header/MobileNav.jsx +++ b/client/modules/IDE/components/Header/MobileNav.jsx @@ -9,6 +9,7 @@ import { ParentMenuContext } from '../../../../components/Menubar/contexts'; import Menubar from '../../../../components/Menubar/Menubar'; import { useMenuProps } from '../../../../components/Menubar/MenubarSubmenu'; import NavMenuItem from '../../../../components/Menubar/MenubarItem'; +import ButtonOrLink from '../../../../common/ButtonOrLink'; import { prop, remSize } from '../../../../theme'; import AsteriskIcon from '../../../../images/p5-asterisk.svg'; import IconButton from '../../../../common/IconButton'; @@ -200,6 +201,15 @@ const LanguageSelect = styled.div` } `; +// TO DO: replace with a more robust component for mobile menu items; this is a temp fix +// because of context-related errors when using MenubarItem in mobile configurations +// eslint-disable-next-line react/prop-types +const MobileMenuItem = ({ children, ...props }) => ( +
  • + {children} +
  • +); + const MobileNav = () => { const project = useSelector((state) => state.project); const user = useSelector((state) => state.user); @@ -294,12 +304,12 @@ const StuffMenu = () => {
      - newSketch()}> + newSketch()}> {t('DashboardView.NewSketch')} - - setCreateCollectionVisible(true)}> + + setCreateCollectionVisible(true)}> {t('DashboardView.CreateCollection')} - +
    {createCollectionVisible && ( @@ -326,13 +336,13 @@ const AccountMenu = () => {
    • {user.username}
    • - + My Stuff - - Settings - dispatch(logoutUser())}> + + Settings + dispatch(logoutUser())}> Log Out - +
    @@ -397,48 +407,50 @@ const MoreMenu = () => {
      {t('Nav.File.Title')} - {t('Nav.File.New')} + + {t('Nav.File.New')} + - saveSketch(cmRef.current)}> + saveSketch(cmRef.current)}> {t('Common.Save')} - - + + {t('Nav.File.Examples')} - + {t('Nav.Edit.Title')} - + {t('Nav.Edit.TidyCode')} - - + + {t('Nav.Edit.Find')} - + {t('Nav.Sketch.Title')} - dispatch(newFile(rootFile.id))}> + dispatch(newFile(rootFile.id))}> {t('Nav.Sketch.AddFile')} - - dispatch(newFolder(rootFile.id))}> + + dispatch(newFolder(rootFile.id))}> {t('Nav.Sketch.AddFolder')} - + {/* TODO: Add Translations */} Settings - { dispatch(openPreferences()); }} > Preferences - - setIsLanguageModalVisible(true)}> + + setIsLanguageModalVisible(true)}> Language - + {t('Nav.Help.Title')} - dispatch(showKeyboardShortcutModal())}> + dispatch(showKeyboardShortcutModal())}> {t('Nav.Help.KeyboardShortcuts')} - - + + {t('Nav.Help.Reference')} - - {t('Nav.Help.About')} + + {t('Nav.Help.About')}
    From 44c092da6820bf47614963e2004b7f6bd13264e9 Mon Sep 17 00:00:00 2001 From: Tristan Espinoza Date: Mon, 12 May 2025 14:38:45 -0700 Subject: [PATCH 091/111] chore: cleaning up docs and comments --- client/components/Menubar/Menubar.jsx | 41 ++------------ client/components/Menubar/MenubarItem.jsx | 36 ++++--------- client/components/Menubar/MenubarSubmenu.jsx | 57 ++++---------------- 3 files changed, 27 insertions(+), 107 deletions(-) diff --git a/client/components/Menubar/Menubar.jsx b/client/components/Menubar/Menubar.jsx index 8d7ff38da0..8a358fceb6 100644 --- a/client/components/Menubar/Menubar.jsx +++ b/client/components/Menubar/Menubar.jsx @@ -11,16 +11,12 @@ import { MenuOpenContext, MenubarContext } from './contexts'; import usePrevious from '../../common/usePrevious'; /** - * @component - * @param {object} props + * Menubar manages a collection of menu items and their submenus. It provides keyboard navigation, + * focus and state management, and other accessibility features for the menu items and submenus. + * * @param {React.ReactNode} props.children - Menu items that will be rendered in the menubar * @param {string} [props.className='nav__menubar'] - CSS class name to apply to the menubar * @returns {JSX.Element} - */ - -/** - * Menubar manages a collection of menu items and their submenus. It provides keyboard navigation, - * focus and state management, and other accessibility features for the menu items and submenus. * * @example * @@ -31,20 +27,16 @@ import usePrevious from '../../common/usePrevious'; */ function Menubar({ children, className }) { - // core state for menu management const [menuOpen, setMenuOpen] = useState('none'); const [activeIndex, setActiveIndex] = useState(0); const prevIndex = usePrevious(activeIndex); const [hasFocus, setHasFocus] = useState(false); - // refs for menu items and their ids const menuItems = useRef(new Set()).current; const menuItemToId = useRef(new Map()).current; - // ref for hiding submenus const timerRef = useRef(null); - // get the id of a menu item by its index const getMenuId = useCallback( (index) => { const items = Array.from(menuItems); @@ -54,9 +46,6 @@ function Menubar({ children, className }) { [menuItems, menuItemToId, activeIndex] ); - /** - * navigation functions - */ const prev = useCallback(() => { const newIndex = (activeIndex - 1 + menuItems.size) % menuItems.size; setActiveIndex(newIndex); @@ -85,8 +74,6 @@ function Menubar({ children, className }) { setActiveIndex(menuItems.size - 1); }, []); - // closes the menu and returns focus to the active menu item - // is called on Escape key press const close = useCallback(() => { if (menuOpen === 'none') return; @@ -96,27 +83,17 @@ function Menubar({ children, className }) { activeNode.focus(); }, [activeIndex, menuItems, menuOpen]); - // toggle the open state of a submenu const toggleMenuOpen = useCallback((id) => { setMenuOpen((prevState) => (prevState === id ? 'none' : id)); }); - /** - * Register top level menu items. Stores both the DOM node and the id of the submenu. - * Access to the DOM node is needed for focus management and tabindex control, - * while the id is needed to toggle the submenu open and closed. - * - * @param {React.RefObject} ref - a ref to the DOM node of the menu item - * @param {string} submenuId - the id of the submenu that the menu item opens - * - */ const registerTopLevelItem = useCallback( (ref, submenuId) => { const menuItemNode = ref.current; if (menuItemNode) { menuItems.add(menuItemNode); - menuItemToId.set(menuItemNode, submenuId); // store the id of the submenu + menuItemToId.set(menuItemNode, submenuId); } return () => { @@ -127,9 +104,6 @@ function Menubar({ children, className }) { [menuItems, menuItemToId] ); - /** - * focus and blur management - */ const clearHideTimeout = useCallback(() => { if (timerRef.current) { clearTimeout(timerRef.current); @@ -164,7 +138,6 @@ function Menubar({ children, className }) { [nodeRef] ); - // keyboard navigation const keyHandlers = { ArrowLeft: (e) => { e.preventDefault(); @@ -183,7 +156,6 @@ function Menubar({ children, className }) { }, Tab: (e) => { e.stopPropagation(); - // close }, Home: (e) => { e.preventDefault(); @@ -195,17 +167,15 @@ function Menubar({ children, className }) { e.stopPropagation(); last(); } - // to do: support direct access keys + // TO DO: support direct access keys }; - // focus the active menu item and set its tabindex useEffect(() => { if (activeIndex !== prevIndex) { const items = Array.from(menuItems); const activeNode = items[activeIndex]; const prevNode = items[prevIndex]; - // roving tabindex prevNode?.setAttribute('tabindex', '-1'); activeNode?.setAttribute('tabindex', '0'); @@ -219,7 +189,6 @@ function Menubar({ children, className }) { clearHideTimeout(); }, [clearHideTimeout]); - // context value for dropdowns and menu items const contextValue = useMemo( () => ({ createMenuHandlers: (menu) => ({ diff --git a/client/components/Menubar/MenubarItem.jsx b/client/components/Menubar/MenubarItem.jsx index 3b0aea6be9..ab3b741e34 100644 --- a/client/components/Menubar/MenubarItem.jsx +++ b/client/components/Menubar/MenubarItem.jsx @@ -4,29 +4,26 @@ import { MenubarContext, SubmenuContext, ParentMenuContext } from './contexts'; import ButtonOrLink from '../../common/ButtonOrLink'; /** + * MenubarItem wraps a button or link in an accessible list item that + * integrates with keyboard navigation and other submenu behaviors. + * + * TO DO: how to document props passed through spread operator? * @component * @param {object} props * @param {string} [props.className='nav__dropdown-item'] - CSS class name to apply to the list item * @param {string} props.id - The id of the list item * @param {string} [props.role='menuitem'] - The role of the list item - * @param {boolean} [props.isDisabled=false] - Whether to hide the item + * @param {boolean} [props.isDisabled=false] - Whether to disable the item * @param {boolean} [props.selected=false] - Whether the item is selected * @returns {JSX.Element} - */ - -/** - * MenubarItem wraps a button or link in an accessible list item that - * integrates with keyboard navigation and other submenu behaviors. * - * @example - * ```jsx - * // basic MenubarItem with click handler and keyboard shortcut + * @example Basic MenubarItem with click handler and keyboard shortcut * dispatch(startSketch())}> * Run * {metaKeyName}+Enter * * - * // as an option in a listbox + * @example as an option in a listbox * * {languageKeyToLabel(key)} * - * ``` */ function MenubarItem({ @@ -48,7 +44,6 @@ function MenubarItem({ selected, ...rest }) { - // core context and state management const { createMenuItemHandlers, hasFocus } = useContext(MenubarContext); const { setSubmenuActiveIndex, @@ -57,17 +52,13 @@ function MenubarItem({ } = useContext(SubmenuContext); const parent = useContext(ParentMenuContext); - // ref for the list item const menuItemRef = useRef(null); - // handlers from parent menu const handlers = createMenuItemHandlers(parent); - // role and aria-selected const role = customRole || 'menuitem'; const ariaSelected = role === 'option' ? { 'aria-selected': selected } : {}; - // focus submenu item on mouse enter const handleMouseEnter = () => { if (hasFocus) { const items = Array.from(submenuItems); @@ -78,7 +69,6 @@ function MenubarItem({ } }; - // register with parent submenu for keyboard navigation useEffect(() => { const unregister = registerSubmenuItem(menuItemRef); return unregister; @@ -107,25 +97,21 @@ function MenubarItem({ MenubarItem.propTypes = { ...ButtonOrLink.propTypes, + className: PropTypes.string, id: PropTypes.string, - onClick: PropTypes.func, - value: PropTypes.string, /** * Provides a way to deal with optional items. */ - isDisabled: PropTypes.bool, - className: PropTypes.string, role: PropTypes.oneOf(['menuitem', 'option']), + isDisabled: PropTypes.bool, selected: PropTypes.bool }; MenubarItem.defaultProps = { - onClick: null, - value: null, - isDisabled: false, className: 'nav__dropdown-item', - role: 'menuitem', id: undefined, + role: 'menuitem', + isDisabled: false, selected: false }; diff --git a/client/components/Menubar/MenubarSubmenu.jsx b/client/components/Menubar/MenubarSubmenu.jsx index c751641514..38683f2310 100644 --- a/client/components/Menubar/MenubarSubmenu.jsx +++ b/client/components/Menubar/MenubarSubmenu.jsx @@ -38,16 +38,13 @@ export function useMenuProps(id) { * -----------------------------------------------------------------------------------------------*/ /** - * @component + * MenubarTrigger renders a button that toggles a submenu. It handles keyboard navigation and supports + * screen readers. It needs to be within a submenu context. + * * @param {Object} props * @param {string} [props.role='menuitem'] - The ARIA role of the trigger button * @param {string} [props.hasPopup='menu'] - The ARIA property that indicates the presence of a popup * @returns {JSX.Element} - */ - -/** - * MenubarTrigger renders a button that toggles a submenu. It handles keyboard navigations and supports - * screen readers. It needs to be within a submenu context. * * @example *
  • { const { id, title, first, last } = useContext(SubmenuContext); const { isOpen, handlers } = useMenuProps(id); - // update active index when mouse enters the trigger and the menu has focus const handleMouseEnter = () => { if (hasFocus) { const items = Array.from(menuItems); @@ -88,7 +84,6 @@ const MenubarTrigger = React.forwardRef(({ role, hasPopup, ...props }, ref) => { } }; - // keyboard handlers const handleKeyDown = (e) => { switch (e.key) { case 'ArrowDown': @@ -118,7 +113,6 @@ const MenubarTrigger = React.forwardRef(({ role, hasPopup, ...props }, ref) => { } }; - // register trigger with parent menubar useEffect(() => { const unregister = registerTopLevelItem(ref, id); return unregister; @@ -160,15 +154,12 @@ MenubarTrigger.defaultProps = { * -----------------------------------------------------------------------------------------------*/ /** - * @component + * MenubarList renders the container for menu items in a submenu. It provides context and handles ARIA roles. + * * @param {Object} props * @param {React.ReactNode} props.children - MenubarItems that should be rendered in the list * @param {string} [props.role='menu'] - The ARIA role of the list element * @returns {JSX.Element} - */ - -/** - * MenubarList renders the container for menu items in a submenu. It provides context and handles ARIA roles. * * @example * @@ -208,7 +199,10 @@ MenubarList.defaultProps = { * -----------------------------------------------------------------------------------------------*/ /** - * @component + * MenubarSubmenu manages a triggerable submenu within a menubar. It is a compound component + * that manages the state of the submenu and its items. It also provides keyboard navigation + * and screen reader support. Supports menu and listbox roles. Needs to be a direct child of Menubar. + * * @param {Object} props * @param {React.ReactNode} props.children - A list of menu items that will be rendered in the menubar * @param {string} props.id - The unique id of the submenu @@ -216,12 +210,6 @@ MenubarList.defaultProps = { * @param {string} [props.triggerRole='menuitem'] - The ARIA role of the trigger button * @param {string} [props.listRole='menu'] - The ARIA role of the list element * @returns {JSX.Element} - */ - -/** - * MenubarSubmenu manages a triggerable submenu within a menubar. It is a compound component - * that manages the state of the submenu and its items. It also provides keyboard navigation - * and screen reader support. Supports menu and listbox roles. Needs to be a direct child of Menubar. * * @example * @@ -240,24 +228,18 @@ function MenubarSubmenu({ listRole: customListRole, ...props }) { - // core state for submenu management const { isOpen, handlers } = useMenuProps(id); const [submenuActiveIndex, setSubmenuActiveIndex] = useState(0); const { setMenuOpen, toggleMenuOpen } = useContext(MenubarContext); const submenuItems = useRef(new Set()).current; - // refs for the button and list elements const buttonRef = useRef(null); const listItemRef = useRef(null); - // roles and properties for the button and list elements const triggerRole = customTriggerRole || 'menuitem'; const listRole = customListRole || 'menu'; const hasPopup = listRole === 'listbox' ? 'listbox' : 'menu'; - /** - * navigation functions for the submenu - */ const prev = useCallback(() => { const newIndex = submenuActiveIndex < 0 @@ -286,13 +268,11 @@ function MenubarSubmenu({ } }, [submenuItems]); - // activate the selected item const activate = useCallback(() => { const items = Array.from(submenuItems); - const activeItem = items[submenuActiveIndex]; // get the active item + const activeItem = items[submenuActiveIndex]; if (activeItem) { - // since active item is a
  • element, we need to get the button or link inside it const activeItemNode = activeItem.firstChild; const isDisabled = @@ -306,8 +286,6 @@ function MenubarSubmenu({ toggleMenuOpen(id); - // check if buttonRef is available and focus it - // we check because the button might be unmounted when activating a link or button if (buttonRef.current) { buttonRef.current.focus(); } @@ -322,12 +300,6 @@ function MenubarSubmenu({ } }, [buttonRef]); - /** - * Register submenu items for keyboard navigation. - * - * @param {React.RefObject} ref - a ref to the DOM node of the menu item - * - */ const registerSubmenuItem = useCallback( (ref) => { const submenuItemNode = ref.current; @@ -343,7 +315,6 @@ function MenubarSubmenu({ [submenuItems] ); - // key handlers for submenu navigation const keyHandlers = { ArrowUp: (e) => { if (!isOpen) return; @@ -377,16 +348,13 @@ function MenubarSubmenu({ close(); }, Tab: (e) => { - // close if (!isOpen) return; - // e.preventDefault(); e.stopPropagation(); setMenuOpen('none'); } - // support direct access keys + // TO DO: direct access keys }; - // our custom keydown handler const handleKeyDown = useCallback( (e) => { if (!isOpen) return; @@ -400,14 +368,12 @@ function MenubarSubmenu({ [isOpen, keyHandlers] ); - // reset submenu active index when submenu is closed useEffect(() => { if (!isOpen) { setSubmenuActiveIndex(-1); } }, [isOpen]); - // add keydown event listener to list when submenu is open useEffect(() => { const el = listItemRef.current; if (!el) return () => {}; @@ -418,7 +384,6 @@ function MenubarSubmenu({ }; }, [isOpen, keyHandlers]); - // focus the active item when submenu is open useEffect(() => { if (isOpen && submenuItems.size > 0) { const items = Array.from(submenuItems); From 3caa2c9828e53ef52bfe646dfad47f8c08823b33 Mon Sep 17 00:00:00 2001 From: Tristan Espinoza Date: Thu, 15 May 2025 14:10:50 -0700 Subject: [PATCH 092/111] chore: updated snapshots and fixed lint errors --- .../IDE/components/Header/MobileNav.jsx | 1 - .../__snapshots__/Nav.unit.test.jsx.snap | 154 +++++++++++++++--- 2 files changed, 130 insertions(+), 25 deletions(-) diff --git a/client/modules/IDE/components/Header/MobileNav.jsx b/client/modules/IDE/components/Header/MobileNav.jsx index f792dd158f..dfb0209c1b 100644 --- a/client/modules/IDE/components/Header/MobileNav.jsx +++ b/client/modules/IDE/components/Header/MobileNav.jsx @@ -8,7 +8,6 @@ import classNames from 'classnames'; import { ParentMenuContext } from '../../../../components/Menubar/contexts'; import Menubar from '../../../../components/Menubar/Menubar'; import { useMenuProps } from '../../../../components/Menubar/MenubarSubmenu'; -import NavMenuItem from '../../../../components/Menubar/MenubarItem'; import ButtonOrLink from '../../../../common/ButtonOrLink'; import { prop, remSize } from '../../../../theme'; import AsteriskIcon from '../../../../images/p5-asterisk.svg'; 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 faa260aa8b..2a85f86887 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 @@ -362,52 +362,105 @@ exports[`Nav renders dashboard version for mobile 1`] = ` File
  • - New +
  • - Save +
  • - Examples + + Examples +
  • Edit
  • - Tidy Code +
  • - Find +
  • Sketch
  • - Add File +
  • - Add Folder +
  • Settings
  • - Preferences +
  • - Language +
  • Help
  • - Keyboard Shortcuts +
  • - Reference + + Reference +
  • - About + + About +
  • @@ -904,52 +957,105 @@ exports[`Nav renders editor version for mobile 1`] = ` File
  • - New +
  • - Save +
  • - Examples + + Examples +
  • Edit
  • - Tidy Code +
  • - Find +
  • Sketch
  • - Add File +
  • - Add Folder +
  • Settings
  • - Preferences +
  • - Language +
  • Help
  • - Keyboard Shortcuts +
  • - Reference + + Reference +
  • - About + + About +
  • From 5477eb41b68e42e4c589e22348c0c3b11e622d47 Mon Sep 17 00:00:00 2001 From: Yugal Kaushik Date: Sat, 17 May 2025 14:22:17 +0530 Subject: [PATCH 093/111] Added translation for fr-CA --- translations/locales/fr-CA/translations.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/translations/locales/fr-CA/translations.json b/translations/locales/fr-CA/translations.json index 740edde1d9..7a43417bb7 100644 --- a/translations/locales/fr-CA/translations.json +++ b/translations/locales/fr-CA/translations.json @@ -139,6 +139,7 @@ "Settings": "Paramètres", "GeneralSettings": "Paramètres généraux", "Accessibility": "Accessibilité", + "LibraryManagement": "Gestion de bibliothèque", "Theme": "Thème", "LightTheme": "Clair", "LightThemeARIA": "Thème clair activé", @@ -176,6 +177,12 @@ "TextOutputARIA": "sortie texte activée", "TableText": "Tableau de texte", "TableOutputARIA": "sortie tableau de texte activée", + "LibraryVersion": "Version de p5.js", + "LibraryVersionInfo": "Une [nouvelle version 2.0](https://github.com/processing/p5.js/releases/) de p5.js est disponible ! Elle deviendra la version par défaut en août 2026, alors profitez de ce temps pour la tester et signaler les bogues. Intéressé à migrer vos esquisses de 1.x vers 2.0 ? Consultez les [ressources de compatibilité et de transition.](https://github.com/processing/p5.js-compatibility)", + "SoundAddon": "p5.sound.js Add-on Bibliothèque", + "PreloadAddon": "p5.js 1.x Compatibility Add-on Bibliothèque — Préchargement", + "ShapesAddon": "p5.js 1.x Compatibility Add-on Bibliothèque — Formes", + "DataAddon": "p5.js 1.x Compatibility Add-on Bibliothèque — Structures de données", "Sound": "Son", "SoundOutputARIA": "sortie son activée" }, From 51b7ba0b0984a29b11d380a2ccaa002ed7a4b418 Mon Sep 17 00:00:00 2001 From: Yugal Kaushik Date: Sat, 17 May 2025 14:27:03 +0530 Subject: [PATCH 094/111] Added translation for hindi --- translations/locales/hi/translations.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/translations/locales/hi/translations.json b/translations/locales/hi/translations.json index ba583b01f6..7324473425 100644 --- a/translations/locales/hi/translations.json +++ b/translations/locales/hi/translations.json @@ -150,6 +150,7 @@ "Settings": "सेटिंग्स", "GeneralSettings": "सामान्य सेटिंग्स", "Accessibility": "ऐक्सेसबिलिटी", + "LibraryManagement": "लाइब्रेरी प्रबंधन", "Theme": "थीम", "LightTheme": "लाइट", "LightThemeARIA": "लाइट थीम चालू", @@ -191,7 +192,13 @@ "PlainText": "प्लेन-टेक्स्ट", "TextOutputARIA": "टेक्स्ट आउटपुट चालू", "TableText": "टेबल-टेक्स्ट", - "TableOutputARIA": "टेबल आउटपुट चालू" + "TableOutputARIA": "टेबल आउटपुट चालू", + "LibraryVersion": "p5.js संस्करण", + "LibraryVersionInfo": "p5.js का एक [नया 2.0 संस्करण](https://github.com/processing/p5.js/releases/) उपलब्ध है! यह अगस्त 2026 में डिफ़ॉल्ट बन जाएगा, इसलिए इस समय का उपयोग इसे आज़माने और बग्स की रिपोर्ट करने के लिए करें। क्या आप 1.x से 2.0 में स्केच को स्थानांतरित करने में रुचि रखते हैं? [संगतता और स्थानांतरण संसाधनों](https://github.com/processing/p5.js-compatibility) को देखें।", + "SoundAddon": "p5.sound.js Add-on लाइब्रेरी", + "PreloadAddon": "p5.js 1.x Compatibility Add-on लाइब्रेरी — प्रीलोड", + "ShapesAddon": "p5.js 1.x Compatibility Add-on लाइब्रेरी — आकार", + "DataAddon": "p5.js 1.x Compatibility Add-on लाइब्रेरी — डेटा संरचनाएँ" }, "KeyboardShortcuts": { "Title": " कीबोर्ड शॉर्टकट", From d14590e7beeddf466219dd8c25db776eda6f0896 Mon Sep 17 00:00:00 2001 From: Yugal Kaushik Date: Sat, 17 May 2025 14:28:21 +0530 Subject: [PATCH 095/111] removed dublicate objects in hindi translation --- translations/locales/hi/translations.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/translations/locales/hi/translations.json b/translations/locales/hi/translations.json index 7324473425..82b417e357 100644 --- a/translations/locales/hi/translations.json +++ b/translations/locales/hi/translations.json @@ -96,10 +96,6 @@ "NewP5": "p5.js पर नये?", "Report": "बग रिपोर्ट", "Learn": "सीखें", - "Twitter": "ट्विटर", - "Home": "होम", - "Instagram": "इंस्टाग्राम", - "Discord": "डिस्कॉर्ड", "WebEditor": "वेब संपादक", "Resources": "साधन", "Libraries": "लाइब्रेरीज़", From 6ed9affb411caf7dedb2767cd3f249ab74598faf Mon Sep 17 00:00:00 2001 From: Yugal Kaushik Date: Sat, 17 May 2025 14:38:51 +0530 Subject: [PATCH 096/111] Added translation for Italiano --- translations/locales/it/translations.json | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/translations/locales/it/translations.json b/translations/locales/it/translations.json index 58a9e104b3..bed94ddb79 100644 --- a/translations/locales/it/translations.json +++ b/translations/locales/it/translations.json @@ -138,6 +138,7 @@ "Settings": "Impostazioni", "GeneralSettings": "Impostazioni generali", "Accessibility": "Accessibilità", + "LibraryManagement": "Gestione della libreria", "Theme": "Tema", "LightTheme": "Chiaro", "LightThemeARIA": "Tema chiaro attivo", @@ -153,9 +154,9 @@ "FontSize": "Dimensione carattere", "SetFontSize": "imposta dimensione carattere", "Autosave": "Auto-salvataggio", - "On": "On", + "On": "Acceso", "AutosaveOnARIA": "auto-salvataggio attivo", - "Off": "Off", + "Off": "Spento", "AutosaveOffARIA": "auto-salvataggio disabilitato", "AutocloseBracketsQuotes": "Auto-chiusura parentesi e virgolette", "AutocloseBracketsQuotesOnARIA": "Auto-chiusura parentesi e virgolette attivo", @@ -176,7 +177,13 @@ "PlainText": "Testo normale", "TextOutputARIA": "output di testo attivo", "TableText": "Testo in tabella", - "TableOutputARIA": "Testo in tabella attivo" + "TableOutputARIA": "Testo in tabella attivo", + "LibraryVersion": "Versione di p5.js", + "LibraryVersionInfo": "È disponibile una [nuova versione 2.0](https://github.com/processing/p5.js/releases/) di p5.js! Diventerà la versione predefinita ad agosto 2026, quindi approfitta di questo tempo per provarla e segnalare eventuali bug. Interessato a migrare gli schizzi da 1.x a 2.0? Consulta le [risorse di compatibilità e transizione.](https://github.com/processing/p5.js-compatibility)", + "SoundAddon": "p5.sound.js Add-on", + "PreloadAddon": "p5.js 1.x Compatibility Add-on Library — Precaricamento", + "ShapesAddon": "p5.js 1.x Compatibility Add-on Library — Forme", + "DataAddon": "p5.js 1.x Compatibility Add-on Library — Strutture di dati" }, "KeyboardShortcuts": { "Title": " Scorciatoie tastiera", From f504f6491c0c4fd0b6b3724ff5666d97c343a887 Mon Sep 17 00:00:00 2001 From: kit <1304340+ksen0@users.noreply.github.com> Date: Mon, 19 May 2025 17:13:19 +0100 Subject: [PATCH 097/111] Add new 1.11.x patches --- client/modules/IDE/hooks/useP5Version.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/modules/IDE/hooks/useP5Version.jsx b/client/modules/IDE/hooks/useP5Version.jsx index 6f97b13715..3e941c3b54 100644 --- a/client/modules/IDE/hooks/useP5Version.jsx +++ b/client/modules/IDE/hooks/useP5Version.jsx @@ -11,6 +11,8 @@ export const p5Versions = [ '2.0.2', '2.0.1', '2.0.0', + '1.11.7', + '1.11.6', '1.11.5', '1.11.4', '1.11.3', From 82ea90c414fc5e8c3502a5b1c1cea63137f1874b Mon Sep 17 00:00:00 2001 From: ksen0 Date: Mon, 19 May 2025 17:35:38 +0100 Subject: [PATCH 098/111] Update default links --- server/domain-objects/createDefaultFiles.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/domain-objects/createDefaultFiles.js b/server/domain-objects/createDefaultFiles.js index 5055f93fac..37349dfd01 100644 --- a/server/domain-objects/createDefaultFiles.js +++ b/server/domain-objects/createDefaultFiles.js @@ -9,8 +9,8 @@ function draw() { export const defaultHTML = ` - - + + From f0bf52abf532643eeb2d5e436d93743a73918930 Mon Sep 17 00:00:00 2001 From: Takshit Saini <94343242+takshittt@users.noreply.github.com> Date: Wed, 21 May 2025 02:31:33 +0530 Subject: [PATCH 099/111] hindi translation of about page and remaining parts of p5.js --- .../IDE/components/KeyboardShortcutModal.jsx | 18 +++-- translations/locales/be/translations.json | 2 +- translations/locales/de/translations.json | 2 +- translations/locales/en-US/translations.json | 6 +- translations/locales/es-419/translations.json | 2 +- translations/locales/fr-CA/translations.json | 2 +- translations/locales/hi/translations.json | 67 +++++++++++++++---- translations/locales/it/translations.json | 2 +- translations/locales/ja/translations.json | 2 +- translations/locales/ko/translations.json | 2 +- translations/locales/pt-BR/translations.json | 2 +- translations/locales/sv/translations.json | 2 +- translations/locales/tr/translations.json | 2 +- translations/locales/uk-UA/translations.json | 2 +- translations/locales/ur/translations.json | 2 +- translations/locales/zh-CN/translations.json | 2 +- translations/locales/zh-TW/translations.json | 2 +- 17 files changed, 85 insertions(+), 34 deletions(-) diff --git a/client/modules/IDE/components/KeyboardShortcutModal.jsx b/client/modules/IDE/components/KeyboardShortcutModal.jsx index fbca28a572..8d402abad9 100644 --- a/client/modules/IDE/components/KeyboardShortcutModal.jsx +++ b/client/modules/IDE/components/KeyboardShortcutModal.jsx @@ -76,7 +76,9 @@ function KeyboardShortcutModal() { {t('KeyboardShortcuts.CodeEditing.CreateNewFile')} -

    General

    +

    + {t('KeyboardShortcuts.General')} +

    • {metaKeyName} + S @@ -86,29 +88,33 @@ function KeyboardShortcutModal() { {metaKeyName} + Enter - {t('KeyboardShortcuts.General.StartSketch')} + {t('KeyboardShortcuts.GeneralSelection.StartSketch')}
    • {metaKeyName} + Shift + Enter - {t('KeyboardShortcuts.General.StopSketch')} + {t('KeyboardShortcuts.GeneralSelection.StopSketch')}
    • {metaKeyName} + Shift + 1 - {t('KeyboardShortcuts.General.TurnOnAccessibleOutput')} + + {t('KeyboardShortcuts.GeneralSelection.TurnOnAccessibleOutput')} +
    • {metaKeyName} + Shift + 2 - {t('KeyboardShortcuts.General.TurnOffAccessibleOutput')} + + {t('KeyboardShortcuts.GeneralSelection.TurnOffAccessibleOutput')} +
    • Shift + Right - Go to Reference for Selected Item in Hinter + {t('KeyboardShortcuts.GeneralSelection.Reference')}
    diff --git a/translations/locales/be/translations.json b/translations/locales/be/translations.json index e987051129..12d0b58658 100644 --- a/translations/locales/be/translations.json +++ b/translations/locales/be/translations.json @@ -203,7 +203,7 @@ "CodeEditing": "কোড এডিটিং", "ColorPicker": "ইনলাইন রঙ নির্বাচক দেখান" }, - "General": { + "GeneralSelection": { "StartSketch": "স্কেচ শুরু", "StopSketch": "স্কেচ বন্ধ", "TurnOnAccessibleOutput": "ব্যবহারযোগ্য আউটপুট চালু করুন", diff --git a/translations/locales/de/translations.json b/translations/locales/de/translations.json index e97539a180..1447243a2c 100644 --- a/translations/locales/de/translations.json +++ b/translations/locales/de/translations.json @@ -193,7 +193,7 @@ "FindPreviousTextMatch": "Vorherigen Text-Treffer finden", "CodeEditing": "Code editieren" }, - "General": { + "GeneralSelection": { "StartSketch": "Sketch starten", "StopSketch": "Sketch stoppen", "TurnOnAccessibleOutput": "Barrierefreie Ausgabe einschalten", diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json index 85359d670c..376c18471c 100644 --- a/translations/locales/en-US/translations.json +++ b/translations/locales/en-US/translations.json @@ -251,11 +251,13 @@ "ColorPicker": "Show Inline Color Picker", "CreateNewFile": "Create New File" }, - "General": { + "General": "General", + "GeneralSelection": { "StartSketch": "Start Sketch", "StopSketch": "Stop Sketch", "TurnOnAccessibleOutput": "Turn On Accessible Output", - "TurnOffAccessibleOutput": "Turn Off Accessible Output" + "TurnOffAccessibleOutput": "Turn Off Accessible Output", + "Reference": "Go to Reference for Selected Item in Hinter" } }, "Sidebar": { diff --git a/translations/locales/es-419/translations.json b/translations/locales/es-419/translations.json index 8e0dd0a44c..830a3b917b 100644 --- a/translations/locales/es-419/translations.json +++ b/translations/locales/es-419/translations.json @@ -200,7 +200,7 @@ "FindPreviousTextMatch": "Encontrar la ocurrencia previa de texto", "CodeEditing": "Editando Código" }, - "General": { + "GeneralSelection": { "StartSketch": "Iniciar bosquejo", "StopSketch": "Detener bosquejo", "TurnOnAccessibleOutput": "Activar salida accesible", diff --git a/translations/locales/fr-CA/translations.json b/translations/locales/fr-CA/translations.json index 740edde1d9..92c3718cf1 100644 --- a/translations/locales/fr-CA/translations.json +++ b/translations/locales/fr-CA/translations.json @@ -196,7 +196,7 @@ "FindPreviousTextMatch": "Correspondance texte précédente", "CodeEditing": "Édition de code" }, - "General": { + "GeneralSelection": { "StartSketch": "Exécuter le croquis", "StopSketch": "Arrêter le croquis", "TurnOnAccessibleOutput": "Activer la sortie accessible", diff --git a/translations/locales/hi/translations.json b/translations/locales/hi/translations.json index ba583b01f6..0f151a07ac 100644 --- a/translations/locales/hi/translations.json +++ b/translations/locales/hi/translations.json @@ -92,14 +92,13 @@ "About": { "Title": "के बारे में", "TitleHelmet": "p5.js वेब एडिटर | के बारे में", + "Headline": "p5.js एडिटर के साथ p5.js स्केच बनाएं, शेयर करें और रीमिक्स करें।", + "IntroDescription1": "p5.js एक मुफ्त, ओपन-सोर्स जावास्क्रिप्ट लाइब्रेरी है जिससे कोडिंग सीखने और कला बनाने में मदद मिलती है। p5.js एडिटर का उपयोग करके, आप बिना कुछ भी डाउनलोड या कॉन्फ़िगर किए p5.js स्केच बना सकते हैं, शेयर कर सकते हैं और रीमिक्स कर सकते हैं।", + "IntroDescription2": "हमारा मानना है कि सॉफ़्टवेयर और इसे सीखने के उपकरण यथासंभव खुले और समावेशी होने चाहिए। आप प्रोसेसिंग फाउंडेशन को दान करके इस कार्य का समर्थन कर सकते हैं, जो संगठन p5.js का समर्थन करता है। आपका दान p5.js के लिए सॉफ़्टवेयर विकास, कोड उदाहरण और ट्यूटोरियल जैसे शिक्षा संसाधन, फेलोशिप और सामुदायिक कार्यक्रमों का समर्थन करता है।", "Contribute": "योगदान", "NewP5": "p5.js पर नये?", "Report": "बग रिपोर्ट", "Learn": "सीखें", - "Twitter": "ट्विटर", - "Home": "होम", - "Instagram": "इंस्टाग्राम", - "Discord": "डिस्कॉर्ड", "WebEditor": "वेब संपादक", "Resources": "साधन", "Libraries": "लाइब्रेरीज़", @@ -111,7 +110,29 @@ "Discord": "डिस्कॉर्ड", "PrivacyPolicy": "गोपनीयता नीति", "TermsOfUse": "उपयोग की शर्तें", - "CodeOfConduct": "आचार संहिता" + "CodeOfConduct": "आचार संहिता", + "DiscordCTA": "डिस्कॉर्ड से जुड़ें", + "ForumCTA": "समूह से जुड़ें", + "Socials": "सोशल मीडिया", + "Email": "ईमेल", + "Youtube": "यूट्यूब", + "Github": "गिटहब", + "Reference": "रेफरेंस", + "Donate": "दान करें", + "GetInvolved": "शामिल हों", + "X": "एक्स", + "LinkDescriptions": { + "Home": "p5.js और हमारे समुदाय के बारे में अधिक जानें।", + "Examples": "संक्षिप्त उदाहरणों के साथ p5.js की संभावनाओं का पता लगाएं।", + "CodeOfConduct": "हमारी सामुदायिक स्थिति और आचार संहिता पढ़ें।", + "Libraries": "समुदाय द्वारा बनाई गई लाइब्रेरीज़ के साथ p5.js की संभावनाओं का विस्तार करें।", + "Reference": "p5.js कोड के हर हिस्से के लिए आसान व्याख्याएं खोजें।", + "Donate": "प्रोसेसिंग फाउंडेशन को दान देकर इस काम का समर्थन करें।", + "Contribute": "गिटहब पर ओपन-सोर्स p5.js एडिटर में योगदान दें।", + "Report": "p5.js एडिटर के साथ टूटे या गलत व्यवहार की रिपोर्ट करें।", + "Forum": "समुदाय द्वारा बनाई गई लाइब्रेरीज़ के साथ p5.js की संभावनाओं का विस्तार करें।", + "Discord": "समुदाय द्वारा बनाई गई लाइब्रेरीज़ के साथ p5.js की संभावनाओं का विस्तार करें।" + } }, "Toast": { "OpenedNewSketch": "नया स्केच खोला", @@ -135,7 +156,10 @@ "StopSketchARIA": "स्केच बंद करे", "EditSketchARIA": "स्केच का नाम संपादित करें", "NewSketchNameARIA": "नया स्केच नाम", - "By": " द्वारा " + "By": " द्वारा ", + "CustomLibraryVersion": "कस्टम p5.js संस्करण", + "VersionPickerARIA": "संस्करण चयनकर्ता", + "NewVersionPickerARIA": "संस्करण चयनकर्ता" }, "Console": { "Title": "कंसोल", @@ -150,6 +174,7 @@ "Settings": "सेटिंग्स", "GeneralSettings": "सामान्य सेटिंग्स", "Accessibility": "ऐक्सेसबिलिटी", + "LibraryManagement": "लाइब्रेरी प्रबंधन", "Theme": "थीम", "LightTheme": "लाइट", "LightThemeARIA": "लाइट थीम चालू", @@ -191,7 +216,21 @@ "PlainText": "प्लेन-टेक्स्ट", "TextOutputARIA": "टेक्स्ट आउटपुट चालू", "TableText": "टेबल-टेक्स्ट", - "TableOutputARIA": "टेबल आउटपुट चालू" + "TableOutputARIA": "टेबल आउटपुट चालू", + "LibraryVersion": "p5.js संस्करण", + "LibraryVersionInfo": "p5.js का [नया 2.0 रिलीज़](https://github.com/processing/p5.js/releases/) उपलब्ध है! यह अगस्त 2026 में डिफ़ॉल्ट बन जाएगा, इसलिए इस समय का उपयोग इसे परीक्षण करने और बग्स की रिपोर्ट करने के लिए करें। 1.x से 2.0 तक स्केच को परिवर्तित करने में रुचि रखते हैं? [संगतता और संक्रमण संसाधनों](https://github.com/processing/p5.js-compatibility) को देखें।", + "CustomVersionTitle": "अपनी लाइब्रेरीज़ का प्रबंधन कर रहे हैं? बढ़िया!", + "CustomVersionInfo": "p5.js का संस्करण वर्तमान में index.html के कोड में प्रबंधित किया जा रहा है। इसका मतलब है कि इसे इस टैब से समायोजित नहीं किया जा सकता।", + "CustomVersionReset": "यदि आप डिफॉल्ट लाइब्रेरीज़ का उपयोग करना चाहते हैं, तो आप index.html में स्क्रिप्ट टैग को निम्नलिखित से बदल सकते हैं:", + "SoundAddon": "p5.sound.js ऐड-ऑन लाइब्रेरी", + "PreloadAddon": "p5.js 1.x संगतता ऐड-ऑन लाइब्रेरी — प्रीलोड", + "ShapesAddon": "p5.js 1.x संगतता ऐड-ऑन लाइब्रेरी — आकार", + "DataAddon": "p5.js 1.x संगतता ऐड-ऑन लाइब्रेरी — डेटा स्ट्रक्चर", + "AddonOnARIA": "चालू", + "AddonOffARIA": "बंद", + "UndoSoundVersion": "फिर से p5.sound.js का उपयोग करना चाहते हैं? इसे वापस चालू करने से आपके पहले उपयोग किए गए संस्करण को पुनर्स्थापित किया जाएगा।", + "CopyToClipboardSuccess": "क्लिपबोर्ड पर कॉपी किया गया!", + "CopyToClipboardFailure": "हम टेक्स्ट को कॉपी करने में असमर्थ थे, इसे मैन्युअल रूप से चयन करके कॉपी करने का प्रयास करें।" }, "KeyboardShortcuts": { "Title": " कीबोर्ड शॉर्टकट", @@ -209,14 +248,17 @@ "FindNextTextMatch": "अगला शब्द मिलान खोजें", "FindPreviousTextMatch": "पिछला शब्द मिलान खोजें", "CodeEditing": "कोड संपादन", - "ColorPicker": "इनलाइन कलर पिकर दिखाएँ" + "ColorPicker": "इनलाइन कलर पिकर दिखाएँ", + "CreateNewFile": "नया फ़ाइल बनाएँ" }, - "General": { + "General": "सामान्य", + "GeneralSelection": { "StartSketch": "स्केच शुरू करें", "StopSketch": "स्केच रोकें", - "TurnOnAccessibleOutput": "Accessibile आउटपुट चालू करें", - "TurnOffAccessibleOutput": "Accessibile आउटपुट बंद करें" + "TurnOnAccessibleOutput": "सुलभ आउटपुट चालू करें", + "TurnOffAccessibleOutput": "सुलभ आउटपुट बंद करें", + "Reference": "हिंटर में चुने गए आइटम के लिए रेफरेंस जाएँ" } }, "Sidebar": { @@ -630,6 +672,7 @@ "PrivacyPolicy": "गोपनीयता नीति", "TermsOfUse": "उपयोग की शर्तें", "CodeOfConduct": "आचार संहिता" - } + }, + "Contact": "संपर्क" } diff --git a/translations/locales/it/translations.json b/translations/locales/it/translations.json index 58a9e104b3..2fdfc3a0c0 100644 --- a/translations/locales/it/translations.json +++ b/translations/locales/it/translations.json @@ -196,7 +196,7 @@ "CodeEditing": "Modifica del codice", "ColorPicker": "Mostra selettore colore in linea" }, - "General": { + "GeneralSelection": { "StartSketch": "Avvia Sketch", "StopSketch": "Ferma Sketch", "TurnOnAccessibleOutput": "Attiva Output accessibile", diff --git a/translations/locales/ja/translations.json b/translations/locales/ja/translations.json index 0683068fb0..8d2030aa46 100644 --- a/translations/locales/ja/translations.json +++ b/translations/locales/ja/translations.json @@ -193,7 +193,7 @@ "FindPreviousTextMatch": "前の一致するテキストを検索", "CodeEditing": "コード編集" }, - "General": { + "GeneralSelection": { "StartSketch": "スケッチを実行", "StopSketch": "スケッチを停止", "TurnOnAccessibleOutput": "アクセシビリティ出力を有効にする", diff --git a/translations/locales/ko/translations.json b/translations/locales/ko/translations.json index 28f5ca6a2e..14f9cff948 100644 --- a/translations/locales/ko/translations.json +++ b/translations/locales/ko/translations.json @@ -181,7 +181,7 @@ "FindPreviousTextMatch": "이전 텍스트 일치 항목 찾기", "CodeEditing": "코드 편집" }, - "General": { + "GeneralSelection": { "StartSketch": "스케치 시작", "StopSketch": "스케치 중지", "TurnOnAccessibleOutput": "접근 가능한 출력 활성화", diff --git a/translations/locales/pt-BR/translations.json b/translations/locales/pt-BR/translations.json index 583d953041..0f6e6ca104 100644 --- a/translations/locales/pt-BR/translations.json +++ b/translations/locales/pt-BR/translations.json @@ -193,7 +193,7 @@ "FindPreviousTextMatch": "Procurar ocorrência de texto anterior", "CodeEditing": "Code Editing" }, - "General": { + "GeneralSelection": { "StartSketch": "Começar Esboço", "StopSketch": "Parar Esboço", "TurnOnAccessibleOutput": "Ativar Saída Acessível", diff --git a/translations/locales/sv/translations.json b/translations/locales/sv/translations.json index f02f2ac4be..62241fbb7e 100644 --- a/translations/locales/sv/translations.json +++ b/translations/locales/sv/translations.json @@ -193,7 +193,7 @@ "FindPreviousTextMatch": "Sök föregående textmatchning", "CodeEditing": "kodredigering" }, - "General": { + "GeneralSelection": { "StartSketch": "Kör sketch", "StopSketch": "Stoppa sketch", "TurnOnAccessibleOutput": "Sätt på tillgänglig output", diff --git a/translations/locales/tr/translations.json b/translations/locales/tr/translations.json index c90ab6bbc1..bf0d00014b 100644 --- a/translations/locales/tr/translations.json +++ b/translations/locales/tr/translations.json @@ -196,7 +196,7 @@ "CodeEditing": "Kod Düzenleme", "ColorPicker": "Satır İçi Renk Seçiciyi Göster" }, - "General": { + "GeneralSelection": { "StartSketch": "Eskizi Başlat", "StopSketch": "Eskizi Durdur", "TurnOnAccessibleOutput": "Erişilebilir Çıktıyı Aç", diff --git a/translations/locales/uk-UA/translations.json b/translations/locales/uk-UA/translations.json index f3d6e9fb1a..58bc180f1b 100644 --- a/translations/locales/uk-UA/translations.json +++ b/translations/locales/uk-UA/translations.json @@ -195,7 +195,7 @@ "FindPreviousTextMatch": "Знайти попередній збіг тексту", "CodeEditing": "Редагування коду" }, - "General": { + "GeneralSelection": { "StartSketch": "Запустити", "StopSketch": "Зупинити", "TurnOnAccessibleOutput": "Увімкнути доступне виведення", diff --git a/translations/locales/ur/translations.json b/translations/locales/ur/translations.json index 1f005e3a42..e91e48996d 100644 --- a/translations/locales/ur/translations.json +++ b/translations/locales/ur/translations.json @@ -194,7 +194,7 @@ "CodeEditing": "کوڈ ایڈیٹنگ", "ColorPicker": "ان لائن رنگ چنندہ دکھائیں۔" }, - "General": { + "GeneralSelection": { "StartSketch": "خاکہ شروع کریں۔", "StopSketch": "خاکہ بند کریں", "TurnOnAccessibleOutput": "قابل رسائی آؤٹ پٹ کو آن کریں۔", diff --git a/translations/locales/zh-CN/translations.json b/translations/locales/zh-CN/translations.json index f7dca3aff3..fc5164e493 100644 --- a/translations/locales/zh-CN/translations.json +++ b/translations/locales/zh-CN/translations.json @@ -196,7 +196,7 @@ "FindPreviousTextMatch": "查找上一个符合的文字", "CodeEditing": "代码编辑" }, - "General": { + "GeneralSelection": { "StartSketch": "运行项目", "StopSketch": "停止运行", "TurnOnAccessibleOutput": "打开无障碍输出", diff --git a/translations/locales/zh-TW/translations.json b/translations/locales/zh-TW/translations.json index 33d8d57570..17ffc05781 100644 --- a/translations/locales/zh-TW/translations.json +++ b/translations/locales/zh-TW/translations.json @@ -196,7 +196,7 @@ "FindPreviousTextMatch": "尋找上一個相符的文字", "CodeEditing": "編輯程式碼" }, - "General": { + "GeneralSelection": { "StartSketch": "執行草稿", "StopSketch": "停止草稿", "TurnOnAccessibleOutput": "啟用輔助輸出", From 04da737bbf4f30d7a3976d4dccf45f73d7d1018f Mon Sep 17 00:00:00 2001 From: Deveshi Dwivedi Date: Thu, 22 May 2025 10:53:56 +0530 Subject: [PATCH 100/111] feat: detect and use browser language as default for first-time users --- client/i18n.js | 19 +++- client/modules/IDE/reducers/preferences.js | 3 +- client/utils/language-utils.js | 107 +++++++++++++++++++++ 3 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 client/utils/language-utils.js diff --git a/client/i18n.js b/client/i18n.js index 722e3856a5..25ce8f19d9 100644 --- a/client/i18n.js +++ b/client/i18n.js @@ -21,6 +21,8 @@ import { enIN } from 'date-fns/locale'; +import getPreferredLanguage from './utils/language-utils'; + const fallbackLng = ['en-US']; export const availableLanguages = [ @@ -42,6 +44,21 @@ export const availableLanguages = [ 'ur' ]; +const detectedLanguage = getPreferredLanguage( + availableLanguages, + fallbackLng[0] +); + +let initialLanguage = detectedLanguage; + +// if user has a saved preference (e.g., from redux or window.__INITIAL_STATE__), use that +if ( + window.__INITIAL_STATE__?.preferences?.language && + availableLanguages.includes(window.__INITIAL_STATE__.preferences.language) +) { + initialLanguage = window.__INITIAL_STATE__.preferences.language; +} + export function languageKeyToLabel(lang) { const languageMap = { be: 'বাংলা', @@ -104,7 +121,7 @@ i18n // .use(LanguageDetector)// to detect the language from currentBrowser .use(Backend) // to fetch the data from server .init({ - lng: 'en-US', + lng: initialLanguage, fallbackLng, // if user computer language is not on the list of available languages, than we will be using the fallback language specified earlier debug: false, backend: options, diff --git a/client/modules/IDE/reducers/preferences.js b/client/modules/IDE/reducers/preferences.js index 630fa465ef..d6323c4fd2 100644 --- a/client/modules/IDE/reducers/preferences.js +++ b/client/modules/IDE/reducers/preferences.js @@ -1,4 +1,5 @@ import * as ActionTypes from '../../../constants'; +import i18n from '../../../i18n'; export const initialState = { tabIndex: 0, @@ -11,7 +12,7 @@ export const initialState = { gridOutput: false, theme: 'light', autorefresh: false, - language: 'en-US', + language: i18n.language, autocloseBracketsQuotes: true, autocompleteHinter: false }; diff --git a/client/utils/language-utils.js b/client/utils/language-utils.js new file mode 100644 index 0000000000..306cabbc3e --- /dev/null +++ b/client/utils/language-utils.js @@ -0,0 +1,107 @@ +/** + * Utility functions for language detection and handling + */ + +function detectLanguageFromUserAgent(userAgent) { + const langRegexes = [ + /\b([a-z]{2}(-[A-Z]{2})?);/i, // matches patterns like "en;" or "en-US;" + /\[([a-z]{2}(-[A-Z]{2})?)\]/i // matches patterns like "[en]" or "[en-US]" + ]; + + const match = langRegexes.reduce((result, regex) => { + if (result) return result; + const matches = userAgent.match(regex); + return matches && matches[1] ? matches[1] : null; + }, null); + + return match; +} + +function getPreferredLanguage(supportedLanguages = [], defaultLanguage = 'en') { + if (typeof navigator === 'undefined') { + return defaultLanguage; + } + + const normalizeLanguage = (langCode) => langCode.toLowerCase().trim(); + + const normalizedSupported = supportedLanguages.map(normalizeLanguage); + + if (navigator.languages && navigator.languages.length) { + const matchedLang = navigator.languages.find((browserLang) => { + const normalizedBrowserLang = normalizeLanguage(browserLang); + + const hasExactMatch = + normalizedSupported.findIndex( + (lang) => lang === normalizedBrowserLang + ) !== -1; + + if (hasExactMatch) { + return true; + } + + const languageOnly = normalizedBrowserLang.split('-')[0]; + const hasLanguageOnlyMatch = + normalizedSupported.findIndex( + (lang) => lang === languageOnly || lang.startsWith(`${languageOnly}-`) + ) !== -1; + + return hasLanguageOnlyMatch; + }); + + if (matchedLang) { + const normalizedMatchedLang = normalizeLanguage(matchedLang); + const exactMatchIndex = normalizedSupported.findIndex( + (lang) => lang === normalizedMatchedLang + ); + + if (exactMatchIndex !== -1) { + return supportedLanguages[exactMatchIndex]; + } + + const languageOnly = normalizedMatchedLang.split('-')[0]; + const languageOnlyMatchIndex = normalizedSupported.findIndex( + (lang) => lang === languageOnly || lang.startsWith(`${languageOnly}-`) + ); + + if (languageOnlyMatchIndex !== -1) { + return supportedLanguages[languageOnlyMatchIndex]; + } + } + } + + if (navigator.language) { + const normalizedNavLang = normalizeLanguage(navigator.language); + const exactMatchIndex = normalizedSupported.findIndex( + (lang) => lang === normalizedNavLang + ); + + if (exactMatchIndex !== -1) { + return supportedLanguages[exactMatchIndex]; + } + + const languageOnly = normalizedNavLang.split('-')[0]; + const languageOnlyMatchIndex = normalizedSupported.findIndex( + (lang) => lang === languageOnly || lang.startsWith(`${languageOnly}-`) + ); + + if (languageOnlyMatchIndex !== -1) { + return supportedLanguages[languageOnlyMatchIndex]; + } + } + + if (navigator.userAgent) { + const userAgentLang = detectLanguageFromUserAgent(navigator.userAgent); + if ( + userAgentLang && + normalizedSupported.includes(normalizeLanguage(userAgentLang)) + ) { + const index = normalizedSupported.indexOf( + normalizeLanguage(userAgentLang) + ); + return supportedLanguages[index]; + } + } + return defaultLanguage; +} + +export default getPreferredLanguage; From e5dc04dc75fd818f7013fc58d060563b10110cd2 Mon Sep 17 00:00:00 2001 From: Deveshi Dwivedi Date: Wed, 28 May 2025 12:55:35 +0530 Subject: [PATCH 101/111] refactor: remove userAgent language detection --- client/utils/language-utils.js | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/client/utils/language-utils.js b/client/utils/language-utils.js index 306cabbc3e..b173a3137f 100644 --- a/client/utils/language-utils.js +++ b/client/utils/language-utils.js @@ -2,21 +2,6 @@ * Utility functions for language detection and handling */ -function detectLanguageFromUserAgent(userAgent) { - const langRegexes = [ - /\b([a-z]{2}(-[A-Z]{2})?);/i, // matches patterns like "en;" or "en-US;" - /\[([a-z]{2}(-[A-Z]{2})?)\]/i // matches patterns like "[en]" or "[en-US]" - ]; - - const match = langRegexes.reduce((result, regex) => { - if (result) return result; - const matches = userAgent.match(regex); - return matches && matches[1] ? matches[1] : null; - }, null); - - return match; -} - function getPreferredLanguage(supportedLanguages = [], defaultLanguage = 'en') { if (typeof navigator === 'undefined') { return defaultLanguage; @@ -89,18 +74,6 @@ function getPreferredLanguage(supportedLanguages = [], defaultLanguage = 'en') { } } - if (navigator.userAgent) { - const userAgentLang = detectLanguageFromUserAgent(navigator.userAgent); - if ( - userAgentLang && - normalizedSupported.includes(normalizeLanguage(userAgentLang)) - ) { - const index = normalizedSupported.indexOf( - normalizeLanguage(userAgentLang) - ); - return supportedLanguages[index]; - } - } return defaultLanguage; } From 5bd599303ae87585ab97b3b846ee96a5070df3c4 Mon Sep 17 00:00:00 2001 From: kit <1304340+ksen0@users.noreply.github.com> Date: Fri, 30 May 2025 14:23:37 +0200 Subject: [PATCH 102/111] Update useP5Version.jsx to include 2.0.3 patch We recently [released p5.js 2.0.3](https://github.com/processing/p5.js/releases/tag/v2.0.3)! This adds it to the version picker. Changes: I have verified that this pull request: * [x] has no linting errors (`npm run lint`) * [x] has no test errors (`npm run test`) * [x] is from a uniquely-named feature branch and is up to date with the `develop` branch. * [ ] is descriptively named and links to an issue number, i.e. `Fixes #123` --- client/modules/IDE/hooks/useP5Version.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/modules/IDE/hooks/useP5Version.jsx b/client/modules/IDE/hooks/useP5Version.jsx index 3e941c3b54..6f38eac625 100644 --- a/client/modules/IDE/hooks/useP5Version.jsx +++ b/client/modules/IDE/hooks/useP5Version.jsx @@ -8,6 +8,7 @@ import PropTypes from 'prop-types'; // JSON.stringify([...document.querySelectorAll('._132722c7')].map(n => n.innerText), null, 2) // TODO: use their API for this to grab these at build time? export const p5Versions = [ + '2.0.3', '2.0.2', '2.0.1', '2.0.0', From ff02050db501028e7a72befcc89d797ee09b4056 Mon Sep 17 00:00:00 2001 From: raclim <43053081+raclim@users.noreply.github.com> Date: Fri, 30 May 2025 12:32:37 -0400 Subject: [PATCH 103/111] 2.16.5 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 84964fb5f1..f8748acd7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "p5.js-web-editor", - "version": "2.16.4", + "version": "2.16.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "p5.js-web-editor", - "version": "2.16.4", + "version": "2.16.5", "license": "LGPL-2.1", "dependencies": { "@auth0/s3": "^1.0.0", diff --git a/package.json b/package.json index 98256b4c4c..2f0293e1fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "p5.js-web-editor", - "version": "2.16.4", + "version": "2.16.5", "description": "The web editor for p5.js.", "scripts": { "clean": "rimraf dist", From 96684b7aa5fb085e732372e07e02b74163149c62 Mon Sep 17 00:00:00 2001 From: Yugal Kaushik Date: Sun, 1 Jun 2025 22:17:04 +0530 Subject: [PATCH 104/111] Fix broken link for preparing an issue --- .github/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 2af5a6d239..6d0fc6757c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -53,7 +53,7 @@ Issues with these labels are a great place to start! - [Need Steps to Reproduce](https://github.com/processing/p5.js-web-editor/labels/Needs%20Steps%20to%20Reproduce) - [Ready for Work](https://github.com/processing/p5.js-web-editor/labels/Ready%20for%20Work) -A breakdown of what each label means can be found in the [Preparing an Issue Guide](#preparing-an-issue). +A breakdown of what each label means can be found in the [Preparing an Issue Guide](../contributor_docs/preparing_an_issue.md). When approaching these issues, know that it's okay to not know how to fix an issue! Feel free to ask questions about to approach the problem. We are all here to learn and make something awesome. Someone from the community will help you out, and asking questions is a great way to learn about the p5.js editor, its file structure, and development process. From b915a196cc0114eef1ed172b28acc3f62162c7a0 Mon Sep 17 00:00:00 2001 From: Yugal Kaushik Date: Mon, 2 Jun 2025 09:25:49 +0530 Subject: [PATCH 105/111] Fix broken link for preparing an issue --- .github/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 6d0fc6757c..01d1f4abad 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -83,7 +83,7 @@ Before submitting a pull request, make sure that: --- ## Ideas for Getting Started -* Use the [p5.js Editor](https://editor.p5js.org)! Find a bug? Think of something you think would add to the project? Reference the [Preparing an Issue Guide](#preparing-an-issue) and open an issue. +* Use the [p5.js Editor](https://editor.p5js.org)! Find a bug? Think of something you think would add to the project? Reference the [Preparing an Issue Guide](../contributor_docs/preparing_an_issue.md) and open an issue. * Expand an existing issue. Sometimes issues are missing steps to reproduce, or need suggestions for potential solutions. Sometimes they need another voice saying, "this is really important!" * Try getting the project running locally on your computer by following the [installation steps](./../contributor_docs/installation.md). * Look through the documentation in the [developer docs](../contributor_docs/) and the [development guide](./../contributor_docs/development.md). Is there anything that could be expanded? Is there anything missing? From 49da364719b172becc461bd995d88b38f6ee30e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 06:16:58 +0000 Subject: [PATCH 106/111] Bump tar-fs from 2.1.2 to 2.1.3 Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 2.1.2 to 2.1.3. - [Commits](https://github.com/mafintosh/tar-fs/commits) --- updated-dependencies: - dependency-name: tar-fs dependency-version: 2.1.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index f8748acd7e..86de2a8c0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38398,9 +38398,9 @@ } }, "node_modules/tar-fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", - "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", "dev": true, "license": "MIT", "dependencies": { @@ -69013,9 +69013,9 @@ } }, "tar-fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", - "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", "dev": true, "requires": { "chownr": "^1.1.1", From cd85ce91d6af838a95137090b917fee873cfe965 Mon Sep 17 00:00:00 2001 From: Yugal Kaushik Date: Tue, 3 Jun 2025 12:38:49 +0530 Subject: [PATCH 107/111] Improve account section terminology and labels --- client/modules/IDE/components/Header/Nav.jsx | 2 +- client/modules/User/components/AccountForm.jsx | 2 +- translations/locales/en-US/translations.json | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/modules/IDE/components/Header/Nav.jsx b/client/modules/IDE/components/Header/Nav.jsx index 2ed8dd224e..151e2d8212 100644 --- a/client/modules/IDE/components/Header/Nav.jsx +++ b/client/modules/IDE/components/Header/Nav.jsx @@ -378,7 +378,7 @@ const AuthenticatedUserMenu = () => { {t('Nav.Auth.MyAssets')} - {t('Preferences.Settings')} + {t('Nav.Auth.MyAccount')} dispatch(logoutUser())}> {t('Nav.Auth.LogOut')} diff --git a/client/modules/User/components/AccountForm.jsx b/client/modules/User/components/AccountForm.jsx index f2f10ec6d5..4406d58c17 100644 --- a/client/modules/User/components/AccountForm.jsx +++ b/client/modules/User/components/AccountForm.jsx @@ -176,7 +176,7 @@ function AccountForm() { )} )} diff --git a/translations/locales/en-US/translations.json b/translations/locales/en-US/translations.json index 376c18471c..afbb6e5057 100644 --- a/translations/locales/en-US/translations.json +++ b/translations/locales/en-US/translations.json @@ -367,13 +367,13 @@ "CurrentPasswordARIA": "Current Password", "NewPassword": "New Password", "NewPasswordARIA": "New Password", - "SubmitSaveAllSettings": "Save All Settings" + "SaveAccountDetails": "Save Account Details" }, "AccountView": { "SocialLogin": "Social Login", "SocialLoginDescription": "Use your GitHub or Google account to log into the p5.js Web Editor.", "Title": "p5.js Web Editor | Account Settings", - "Settings": "Account Settings", + "Settings": "My Account", "AccountTab": "Account", "AccessTokensTab": "Access Tokens" }, From b8e1895b3d4c581d56f085585c8666e35a450d36 Mon Sep 17 00:00:00 2001 From: Yugal Kaushik Date: Tue, 3 Jun 2025 12:58:12 +0530 Subject: [PATCH 108/111] Update AccountForum submit button test --- .../User/components/AccountForm.unit.test.jsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/modules/User/components/AccountForm.unit.test.jsx b/client/modules/User/components/AccountForm.unit.test.jsx index 653dce5243..caaa7ddc39 100644 --- a/client/modules/User/components/AccountForm.unit.test.jsx +++ b/client/modules/User/components/AccountForm.unit.test.jsx @@ -61,8 +61,8 @@ describe('', () => { it('handles form submission and calls updateSettings', async () => { subject(); - const saveAllSettingsButton = screen.getByRole('button', { - name: /save all settings/i + const saveAccountDetailsButton = screen.getByRole('button', { + name: /save account details/i }); const currentPasswordElement = screen.getByLabelText(/current password/i); @@ -81,7 +81,7 @@ describe('', () => { }); await act(async () => { - fireEvent.click(saveAllSettingsButton); + fireEvent.click(saveAccountDetailsButton); }); await waitFor(() => { @@ -92,8 +92,8 @@ describe('', () => { it('Save all setting button should get disabled while submitting and enable when not submitting', async () => { subject(); - const saveAllSettingsButton = screen.getByRole('button', { - name: /save all settings/i + const saveAccountDetailsButton = screen.getByRole('button', { + name: /save account details/i }); const currentPasswordElement = screen.getByLabelText(/current password/i); @@ -110,12 +110,12 @@ describe('', () => { value: 'newPassword' } }); - expect(saveAllSettingsButton).not.toHaveAttribute('disabled'); + expect(saveAccountDetailsButton).not.toHaveAttribute('disabled'); await act(async () => { - fireEvent.click(saveAllSettingsButton); + fireEvent.click(saveAccountDetailsButton); await waitFor(() => { - expect(saveAllSettingsButton).toHaveAttribute('disabled'); + expect(saveAccountDetailsButton).toHaveAttribute('disabled'); }); }); }); From 961f038e176845239233c5fff2ad69fedce14121 Mon Sep 17 00:00:00 2001 From: kit <1304340+ksen0@users.noreply.github.com> Date: Mon, 9 Jun 2025 09:57:45 +0200 Subject: [PATCH 109/111] Update useP5Version.jsx to include 1.11.8 patch --- client/modules/IDE/hooks/useP5Version.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/modules/IDE/hooks/useP5Version.jsx b/client/modules/IDE/hooks/useP5Version.jsx index 6f38eac625..03ba3d0da4 100644 --- a/client/modules/IDE/hooks/useP5Version.jsx +++ b/client/modules/IDE/hooks/useP5Version.jsx @@ -12,6 +12,7 @@ export const p5Versions = [ '2.0.2', '2.0.1', '2.0.0', + '1.11.8', '1.11.7', '1.11.6', '1.11.5', From 07a99fe443fd1f4483cf2604810b0bf99f60d045 Mon Sep 17 00:00:00 2001 From: raclim <43053081+raclim@users.noreply.github.com> Date: Mon, 9 Jun 2025 20:21:02 -0400 Subject: [PATCH 110/111] 2.16.6 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 86de2a8c0e..6791d00abf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "p5.js-web-editor", - "version": "2.16.5", + "version": "2.16.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "p5.js-web-editor", - "version": "2.16.5", + "version": "2.16.6", "license": "LGPL-2.1", "dependencies": { "@auth0/s3": "^1.0.0", diff --git a/package.json b/package.json index 2f0293e1fc..21f23d7336 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "p5.js-web-editor", - "version": "2.16.5", + "version": "2.16.6", "description": "The web editor for p5.js.", "scripts": { "clean": "rimraf dist", From 4542c87b5083c4f457acf279fb282b39c24043ea Mon Sep 17 00:00:00 2001 From: Ego Nwaekpe Date: Tue, 10 Jun 2025 19:36:08 +0100 Subject: [PATCH 111/111] Added links to p5.js and Web Editor release pages in the About section --- client/modules/About/pages/About.jsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/client/modules/About/pages/About.jsx b/client/modules/About/pages/About.jsx index 39ef396192..5690aba0a0 100644 --- a/client/modules/About/pages/About.jsx +++ b/client/modules/About/pages/About.jsx @@ -130,10 +130,22 @@ const About = () => { {t('About.CodeOfConduct')}

    - {t('About.WebEditor')}: v{packageData?.version} + + {t('About.WebEditor')}: v{packageData?.version} +

    - p5.js: v{p5version} + + p5.js: v{p5version} +