From a2f039017d18635fb27d235256af83b795ae680c Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 15 Dec 2025 11:46:52 +0200 Subject: [PATCH 01/66] chore: initial commit - making elements focusable and react to enter --- packages/scratch-gui/src/components/menu-bar/menu-bar.jsx | 8 ++++++++ .../scratch-gui/src/components/menu-bar/settings-menu.jsx | 3 +++ 2 files changed, 11 insertions(+) diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx index 58f955a1bb..dd3275787e 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx @@ -289,6 +289,11 @@ class MenuBar extends React.Component { }; } handleKeyPress (event) { + if (event.key === 'Enter' || event.key === '') { + event.preventDefault(); + event.target.click(); + } + const modifier = bowser.mac ? event.metaKey : event.ctrlKey; if (modifier && event.key === 's') { this.props.onClickSave(); @@ -442,6 +447,9 @@ class MenuBar extends React.Component {
Scratch Date: Fri, 19 Dec 2025 15:50:05 +0200 Subject: [PATCH 02/66] feat: added some accessability with arrow logic --- .../context-menu/menu-path-context.jsx | 89 ++++++++ .../scratch-gui/src/components/gui/gui.jsx | 77 +++---- .../src/components/menu-bar/language-menu.jsx | 120 +++++++++-- .../src/components/menu-bar/menu-bar.jsx | 15 +- .../src/components/menu-bar/settings-menu.jsx | 183 ++++++++++++---- .../src/components/menu-bar/theme-menu.jsx | 202 +++++++++++++----- .../scratch-gui/src/components/menu/menu.jsx | 18 +- packages/scratch-gui/src/containers/gui.jsx | 1 + 8 files changed, 563 insertions(+), 142 deletions(-) create mode 100644 packages/scratch-gui/src/components/context-menu/menu-path-context.jsx diff --git a/packages/scratch-gui/src/components/context-menu/menu-path-context.jsx b/packages/scratch-gui/src/components/context-menu/menu-path-context.jsx new file mode 100644 index 0000000000..47121f88b0 --- /dev/null +++ b/packages/scratch-gui/src/components/context-menu/menu-path-context.jsx @@ -0,0 +1,89 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import bindAll from 'lodash.bindall'; + +export const MenuRefContext = React.createContext(null); + +export class MenuRefProvider extends React.Component { + constructor (props) { + super(props); + + this.state = { + openRefs: [] + }; + + bindAll(this, [ + 'addInner', + 'isTopMenu', + 'isOpenMenu', + 'removeAll', + 'removeByRef', + 'removeInner' + ]); + } + + isTopMenu (ref) { + const {openRefs} = this.state; + return openRefs.length > 0 && openRefs[openRefs.length - 1] === ref; + } + + isOpenMenu (ref) { + return this.state.openRefs.includes(ref); + } + + addInner (ref) { + this.setState(prev => ({ + openRefs: [...prev.openRefs, ref] + })); + } + + removeByRef (ref) { + this.setState(prev => { + const refs = prev.openRefs; + const index = refs.indexOf(ref); + + if (index === -1) return {openRefs: refs}; + + return { + openRefs: refs.slice(0, index) + }; + }); + } + + removeInner () { + this.setState(prev => ({ + openRefs: prev.openRefs.slice(0, prev.openRefs.length - 1) + })); + } + + removeAll () { + this.setState({openRefs: []}); + } + + // printChain () { + // console.log(this.state.openRefs); + // } + + render () { + const value = { + openRefs: this.state.openRefs, + isTopMenu: this.isTopMenu, + isOpenMenu: this.isOpenMenu, + addInner: this.addInner, + removeInner: this.removeInner, + removeAll: this.removeAll, + removeByRef: this.removeByRef + // printChain: this.printChain + }; + + return ( + + {this.props.children} + + ); + } +} + +MenuRefProvider.propTypes = { + children: PropTypes.node +}; diff --git a/packages/scratch-gui/src/components/gui/gui.jsx b/packages/scratch-gui/src/components/gui/gui.jsx index 1fa06119e2..2f81df9eed 100644 --- a/packages/scratch-gui/src/components/gui/gui.jsx +++ b/packages/scratch-gui/src/components/gui/gui.jsx @@ -44,6 +44,7 @@ import soundsIcon from './icon--sounds.svg'; import DebugModal from '../debug-modal/debug-modal.jsx'; import {setPlatform} from '../../reducers/platform.js'; import {PLATFORM} from '../../lib/platform.js'; +import {MenuRefProvider} from '../context-menu/menu-path-context.jsx'; // Cache this value to only retrieve it once the first time. // Assume that it doesn't change for a session. @@ -252,42 +253,46 @@ const GUIComponent = props => { onRequestClose={onRequestCloseBackdropLibrary} /> ) : null} - {!menuBarHidden && } + {!menuBarHidden && + + + + } diff --git a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx index 81402c317a..ad2e7f93d4 100644 --- a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx @@ -9,8 +9,9 @@ import locales from 'scratch-l10n'; import check from './check.svg'; import {MenuItem, Submenu} from '../menu/menu.jsx'; import languageIcon from '../language-selector/language-icon.svg'; -import {languageMenuOpen, openLanguageMenu} from '../../reducers/menus.js'; +import {closeLanguageMenu, languageMenuOpen, openLanguageMenu} from '../../reducers/menus.js'; import {selectLocale} from '../../reducers/locales.js'; +import {MenuRefContext} from '../context-menu/menu-path-context.jsx'; import styles from './settings-menu.css'; @@ -20,9 +21,18 @@ class LanguageMenu extends React.PureComponent { constructor (props) { super(props); bindAll(this, [ + 'handleKeyPress', + 'handleKeyPressOpenMenu', + 'handleMove', + 'handleOnOpen', + 'handleOnClose', + 'setFocusedRef', 'setRef', 'handleMouseOver' ]); + + this.state = {focusedIndex: -1}; + this.itemRefs = Object.keys(locales).map(() => React.createRef()); } componentDidUpdate (prevProps) { @@ -32,26 +42,104 @@ class LanguageMenu extends React.PureComponent { } } + static contextType = MenuRefContext; + setRef (component) { this.selectedRef = component; } + handleKeyPress (e) { + if (this.context.isTopMenu(this.props.focusedRef)) { + this.handleKeyPressOpenMenu(e); + } else if (!this.context.isOpenMenu(this.props.focusedRef) && (e.key === ' ' || e.key === 'ArrowRight')) { + e.preventDefault(); + this.handleOnOpen(); + } + } + + handleKeyPressOpenMenu (e) { + if (e.key === 'ArrowDown') { + e.preventDefault(); + this.handleMove(1); + } + if (e.key === 'ArrowUp') { + e.preventDefault(); + this.handleMove(-1); + } + + if (e.key === 'Enter') { + e.preventDefault(); + this.props.onChangeLanguage(Object.keys(locales)[this.state.focusedIndex]); + this.handleOnClose(); + } + + if (e.key === 'ArrowLeft' || e.key === 'Escape') { + e.preventDefault(); + this.handleOnClose(); + } + } + + handleMove (move) { + const newIndex = (this.state.focusedIndex + move + this.itemRefs.length) % this.itemRefs.length; + this.setState({focusedIndex: newIndex}, () => { + const ref = this.itemRefs[this.state.focusedIndex]; + if (ref && ref.current) ref.current.focus(); + }); + } + handleMouseOver () { // If we are using hover rather than clicks for submenus, scroll the selected option into view if (!this.props.menuOpen && this.selectedRef) { this.selectedRef.scrollIntoView({block: 'center'}); + this.setFocusedRef(this.selectedRef); + } + } + + handleOnOpen () { + if (this.context.isOpenMenu(this.props.focusedRef)) return; + + this.props.onRequestOpen(); + this.setState({focusedIndex: Object.keys(locales).indexOf(this.props.currentLocale)}, () => { + this.setFocusedRef(this.itemRefs[this.state.focusedIndex]); + }); + + this.context.addInner(this.props.focusedRef); + } + + handleOnClose () { + this.context.removeByRef(this.props.focusedRef); + this.setState({focusedIndex: -1}, () => { + this.setFocusedRef(this.props.focusedRef); + }); + closeLanguageMenu(); + } + + setFocusedRef (component) { + this.focusedRef = component; + if (this.focusedRef && this.focusedRef.current) { + this.focusedRef.current.focus(); } } render () { + const { + currentLocale, + focusedRef, + isRtl, + onChangeLanguage + } = this.props; + return ( - +
{ Object.keys(locales) - .map(locale => ( - { + const isSelected = currentLocale === locale; + + return ( this.props.onChangeLanguage(locale)} + onClick={() => onChangeLanguage(locale)} + focusedRef={this.itemRefs[index]} + onParentKeyPress={this.handleKeyPress} > {locales[locale].name} - - )) + ); + }) } @@ -101,8 +193,8 @@ class LanguageMenu extends React.PureComponent { LanguageMenu.propTypes = { currentLocale: PropTypes.string, + focusedRef: PropTypes.object, isRtl: PropTypes.bool, - label: PropTypes.string, menuOpen: PropTypes.bool, onChangeLanguage: PropTypes.func, onRequestCloseSettings: PropTypes.func, diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx index dd3275787e..28892afbbc 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx @@ -289,7 +289,7 @@ class MenuBar extends React.Component { }; } handleKeyPress (event) { - if (event.key === 'Enter' || event.key === '') { + if (event.key === 'Enter') { event.preventDefault(); event.target.click(); } @@ -474,6 +474,9 @@ class MenuBar extends React.Component { [styles.active]: this.props.fileMenuOpen })} onClick={this.props.onClickFile} + aria-label="File Menu" + role="button" + tabIndex={0} > @@ -544,6 +547,9 @@ class MenuBar extends React.Component { [styles.active]: this.props.editMenuOpen })} onClick={this.props.onClickEdit} + role="button" + aria-label="Edit Menu" + tabIndex={0} > @@ -596,6 +602,9 @@ class MenuBar extends React.Component { [styles.active]: this.props.modeMenuOpen })} onClick={this.props.onClickMode} + role="button" + aria-label="Mode" + tabIndex={0} >
( -
- - - - - - { + this.setFocusedRef(this.itemRefs[0]); + }); + } + } + + static contextType = MenuRefContext; + + handleOnClose () { + this.context.removeByRef(this.settingsRef); + this.props.onRequestClose(); + this.setState({focusedIndex: -1}); + } + + handleOnOpen () { + if (this.context.isOpenMenu(this.settingsRef)) return; + + this.setState({focusedIndex: 0}, () => { + this.props.onRequestOpen(); + this.context.addInner(this.settingsRef); + this.setFocusedRef(this.itemRefs[0]); + }); + } + + setFocusedRef (component) { + this.focusedRef = component; + if (this.focusedRef && this.focusedRef.current) { + this.focusedRef.current.focus(); + } + } + + handleKeyPress (e) { + if (e.key === 'Tab') { + this.handleOnClose(); + } + + if (this.context.isTopMenu(this.settingsRef)) { + this.handleKeyPressOpenMenu(e); + } else if (!this.context.isOpenMenu(this.settingsRef) && (e.key === ' ' || e.key === 'ArrowRight')) { + e.preventDefault(); + this.handleOnOpen(); + } + } + + handleKeyPressOpenMenu (e) { + if (e.key === 'ArrowLeft' || e.key === 'Escape') { + e.preventDefault(); + this.handleOnClose(); + } + + if (e.key === 'ArrowUp') { + e.preventDefault(); + this.handleMove(-1); + } + + if (e.key === 'ArrowDown') { + e.preventDefault(); + this.handleMove(1); + } + } + + handleMove (direction) { + const nextIndex = + (this.state.focusedIndex + direction + this.itemRefs.length) % this.itemRefs.length; + this.setState({focusedIndex: nextIndex}, () => { + this.setFocusedRef(this.itemRefs[nextIndex]); + }); + } + + render () { + const { + canChangeLanguage, + canChangeTheme, + isRtl, + onRequestClose, + settingsMenuOpen + } = this.props; + + return (
- - {canChangeLanguage && } - {canChangeTheme && } - - -
-); + + + + + + + + {canChangeLanguage && } + {canChangeTheme && } + + +
); + } +}; SettingsMenu.propTypes = { canChangeLanguage: PropTypes.bool, diff --git a/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx b/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx index e9ce24f1de..5aef101be1 100644 --- a/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx @@ -1,5 +1,6 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; +import bindAll from 'lodash.bindall'; import React from 'react'; import {FormattedMessage} from 'react-intl'; import {connect} from 'react-redux'; @@ -8,8 +9,9 @@ import check from './check.svg'; import {MenuItem, Submenu} from '../menu/menu.jsx'; import {DEFAULT_THEME, HIGH_CONTRAST_THEME, themeMap} from '../../lib/themes'; import {persistTheme} from '../../lib/themes/themePersistance'; -import {openThemeMenu, themeMenuOpen} from '../../reducers/menus.js'; +import {openThemeMenu, closeThemeMenu} from '../../reducers/menus.js'; import {setTheme} from '../../reducers/theme.js'; +import {MenuRefContext} from '../context-menu/menu-path-context.jsx'; import styles from './settings-menu.css'; @@ -19,7 +21,11 @@ const ThemeMenuItem = props => { const themeInfo = themeMap[props.theme]; return ( - +
{ ThemeMenuItem.propTypes = { isSelected: PropTypes.bool, onClick: PropTypes.func, - theme: PropTypes.string + theme: PropTypes.string, + focusedRef: PropTypes.object, + onParentKeyPress: PropTypes.func }; -const ThemeMenu = ({ - isRtl, - menuOpen, - onChangeTheme, - onRequestOpen, - theme -}) => { - const enabledThemes = [DEFAULT_THEME, HIGH_CONTRAST_THEME]; - const themeInfo = themeMap[theme]; +class ThemeMenu extends React.PureComponent { + constructor (props) { + super(props); + bindAll(this, [ + 'handleKeyPress', + 'handleKeyPressOpenMenu', + 'handleMove', + 'handleOnOpen', + 'handleOnClose', + 'setFocusedRef', + 'setRef' + ]); - return ( - -
React.createRef()); + } + + static contextType = MenuRefContext; + + setRef (component) { + this.selectedRef = component; + } + + handleKeyPress (e) { + if (this.context.isTopMenu(this.props.focusedRef)) { + this.handleKeyPressOpenMenu(e); + } else if (!this.context.isOpenMenu(this.props.focusedRef) && (e.key === ' ' || e.key === 'ArrowRight')) { + e.preventDefault(); + this.handleOnOpen(); + } + } + + handleKeyPressOpenMenu (e) { + if (e.key === 'ArrowDown') { + e.preventDefault(); + this.handleMove(1); + } + if (e.key === 'ArrowUp') { + e.preventDefault(); + this.handleMove(-1); + } + + if (e.key === 'Enter') { + e.preventDefault(); + this.props.onChangeTheme(this.enabledThemes[this.state.focusedIndex]); + this.handleOnClose(); + } + + if (e.key === 'ArrowLeft' || e.key === 'Escape') { + e.preventDefault(); + this.handleOnClose(); + } + } + + handleMove (move) { + const newIndex = (this.state.focusedIndex + move + this.itemRefs.length) % this.itemRefs.length; + this.setState({focusedIndex: newIndex}, () => { + const ref = this.itemRefs[this.state.focusedIndex]; + if (ref && ref.current) ref.current.focus(); + }); + } + + handleOnOpen () { + if (this.context.isTopMenu(this.props.focusedRef)) return; + + this.props.onRequestOpen(); + this.setState({focusedIndex: 0}, () => { + this.setFocusedRef(this.itemRefs[this.state.focusedIndex]); + }); + + this.context.addInner(this.props.focusedRef); + } + + handleOnClose () { + this.context.removeByRef(this.props.focusedRef); + this.setState({focusedIndex: -1}, () => { + this.setFocusedRef(this.props.focusedRef); + }); + closeThemeMenu(); + } + + setFocusedRef (component) { + this.focusedRef = component; + if (this.focusedRef && this.focusedRef.current) { + this.focusedRef.current.focus(); + } + } + + render () { + const { + focusedRef, + isRtl, + onChangeTheme, + theme + } = this.props; + + const themeInfo = themeMap[theme]; + + return ( + - - - + - - -
- - {enabledThemes.map(enabledTheme => ( - onChangeTheme(enabledTheme)} - theme={enabledTheme} - />) - )} - -
- ); -}; + + + + +
+ + {this.enabledThemes.map((enabledTheme, index) => ( + onChangeTheme(enabledTheme)} + theme={enabledTheme} + focusedRef={this.itemRefs[index]} + onParentKeyPress={this.handleKeyPress} + />) + )} + +
+ ); + } +} ThemeMenu.propTypes = { + focusedRef: PropTypes.object, isRtl: PropTypes.bool, - menuOpen: PropTypes.bool, onChangeTheme: PropTypes.func, // eslint-disable-next-line react/no-unused-prop-types onRequestCloseSettings: PropTypes.func, @@ -99,7 +206,6 @@ ThemeMenu.propTypes = { const mapStateToProps = state => ({ isRtl: state.locales.isRtl, - menuOpen: themeMenuOpen(state), theme: state.scratchGui.theme.theme }); diff --git a/packages/scratch-gui/src/components/menu/menu.jsx b/packages/scratch-gui/src/components/menu/menu.jsx index 5be46a32e3..175c90a370 100644 --- a/packages/scratch-gui/src/components/menu/menu.jsx +++ b/packages/scratch-gui/src/components/menu/menu.jsx @@ -32,7 +32,6 @@ MenuComponent.propTypes = { place: PropTypes.oneOf(['left', 'right']) }; - const Submenu = ({children, className, place, ...props}) => (
(
  • {children}
  • ); MenuItem.propTypes = { + ariaLabel: PropTypes.string, + ariaRole: PropTypes.string, children: PropTypes.node, className: PropTypes.string, expanded: PropTypes.bool, - onClick: PropTypes.func + onClick: PropTypes.func, + focusedRef: PropTypes.object, + onParentKeyPress: PropTypes.func }; diff --git a/packages/scratch-gui/src/containers/gui.jsx b/packages/scratch-gui/src/containers/gui.jsx index d84fab6f7f..375790d47b 100644 --- a/packages/scratch-gui/src/containers/gui.jsx +++ b/packages/scratch-gui/src/containers/gui.jsx @@ -148,6 +148,7 @@ GUI.propTypes = { }; GUI.defaultProps = { + // isTotallyNormal: true, - for testing only isTotallyNormal: false, onStorageInit: () => {}, onProjectLoaded: () => {}, From 34b18dfa9331bbedfca38323e6bf20478ae6b248 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Fri, 19 Dec 2025 17:28:47 +0200 Subject: [PATCH 03/66] chore: addressed copilot stuff --- .../src/components/context-menu/menu-path-context.jsx | 5 ----- .../src/components/menu-bar/language-menu.jsx | 8 +++++--- .../scratch-gui/src/components/menu-bar/menu-bar.jsx | 2 +- .../src/components/menu-bar/settings-menu.jsx | 2 +- .../scratch-gui/src/components/menu-bar/theme-menu.jsx | 10 ++++++---- packages/scratch-gui/src/components/menu/menu.jsx | 2 +- 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/scratch-gui/src/components/context-menu/menu-path-context.jsx b/packages/scratch-gui/src/components/context-menu/menu-path-context.jsx index 47121f88b0..2333f51c9f 100644 --- a/packages/scratch-gui/src/components/context-menu/menu-path-context.jsx +++ b/packages/scratch-gui/src/components/context-menu/menu-path-context.jsx @@ -60,10 +60,6 @@ export class MenuRefProvider extends React.Component { this.setState({openRefs: []}); } - // printChain () { - // console.log(this.state.openRefs); - // } - render () { const value = { openRefs: this.state.openRefs, @@ -73,7 +69,6 @@ export class MenuRefProvider extends React.Component { removeInner: this.removeInner, removeAll: this.removeAll, removeByRef: this.removeByRef - // printChain: this.printChain }; return ( diff --git a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx index ad2e7f93d4..2de5339149 100644 --- a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx @@ -193,12 +193,13 @@ class LanguageMenu extends React.PureComponent { LanguageMenu.propTypes = { currentLocale: PropTypes.string, - focusedRef: PropTypes.object, + focusedRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), isRtl: PropTypes.bool, menuOpen: PropTypes.bool, onChangeLanguage: PropTypes.func, onRequestCloseSettings: PropTypes.func, - onRequestOpen: PropTypes.func + onRequestOpen: PropTypes.func, + onRequestClose: PropTypes.func }; const mapStateToProps = state => ({ @@ -213,7 +214,8 @@ const mapDispatchToProps = (dispatch, ownProps) => ({ dispatch(selectLocale(locale)); ownProps.onRequestCloseSettings(); }, - onRequestOpen: () => dispatch(openLanguageMenu()) + onRequestOpen: () => dispatch(openLanguageMenu()), + onRequestClose: () => dispatch(closeLanguageMenu()) }); export default connect( diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx index 28892afbbc..180a818d9b 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx @@ -449,7 +449,7 @@ class MenuBar extends React.Component { Scratch { this.setFocusedRef(this.props.focusedRef); }); - closeThemeMenu(); + this.props.onRequestClose(); } setFocusedRef (component) { @@ -195,12 +195,13 @@ class ThemeMenu extends React.PureComponent { } ThemeMenu.propTypes = { - focusedRef: PropTypes.object, + focusedRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), isRtl: PropTypes.bool, onChangeTheme: PropTypes.func, // eslint-disable-next-line react/no-unused-prop-types onRequestCloseSettings: PropTypes.func, onRequestOpen: PropTypes.func, + onRequestClose: PropTypes.func, theme: PropTypes.string }; @@ -215,7 +216,8 @@ const mapDispatchToProps = (dispatch, ownProps) => ({ ownProps.onRequestCloseSettings(); persistTheme(theme); }, - onRequestOpen: () => dispatch(openThemeMenu()) + onRequestOpen: () => dispatch(openThemeMenu()), + onRequestClose: () => dispatch(closeThemeMenu()) }); export default connect( diff --git a/packages/scratch-gui/src/components/menu/menu.jsx b/packages/scratch-gui/src/components/menu/menu.jsx index 175c90a370..523d6946c9 100644 --- a/packages/scratch-gui/src/components/menu/menu.jsx +++ b/packages/scratch-gui/src/components/menu/menu.jsx @@ -93,7 +93,7 @@ MenuItem.propTypes = { className: PropTypes.string, expanded: PropTypes.bool, onClick: PropTypes.func, - focusedRef: PropTypes.object, + focusedRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), onParentKeyPress: PropTypes.func }; From 1c05d24c75b468cbf6fdde8df58f87d45ccf0d99 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 22 Dec 2025 16:35:00 +0200 Subject: [PATCH 04/66] feat: refactored dropdown menu logic --- .../context-menu/menu-path-context.jsx | 64 ++++++------ .../src/components/menu-bar/base-menu.jsx | 97 +++++++++++++++++++ .../src/components/menu-bar/language-menu.jsx | 89 +++-------------- .../src/components/menu-bar/settings-menu.jsx | 87 ++--------------- .../src/components/menu-bar/theme-menu.jsx | 92 +++--------------- 5 files changed, 163 insertions(+), 266 deletions(-) create mode 100644 packages/scratch-gui/src/components/menu-bar/base-menu.jsx diff --git a/packages/scratch-gui/src/components/context-menu/menu-path-context.jsx b/packages/scratch-gui/src/components/context-menu/menu-path-context.jsx index 2333f51c9f..7c562428c7 100644 --- a/packages/scratch-gui/src/components/context-menu/menu-path-context.jsx +++ b/packages/scratch-gui/src/components/context-menu/menu-path-context.jsx @@ -9,66 +9,70 @@ export class MenuRefProvider extends React.Component { super(props); this.state = { - openRefs: [] + refStack: [] }; bindAll(this, [ - 'addInner', + 'push', + 'pop', + 'cut', + 'clear', 'isTopMenu', - 'isOpenMenu', - 'removeAll', - 'removeByRef', - 'removeInner' + 'isOpenMenu' ]); } - isTopMenu (ref) { - const {openRefs} = this.state; - return openRefs.length > 0 && openRefs[openRefs.length - 1] === ref; - } + push (ref, depth) { + if (depth <= this.state.refStack.length) { + this.cut(this.state.refStack[depth - 1]); + } - isOpenMenu (ref) { - return this.state.openRefs.includes(ref); + this.setState(prev => ({ + refStack: [...prev.refStack, ref] + })); } - addInner (ref) { + pop () { this.setState(prev => ({ - openRefs: [...prev.openRefs, ref] + stack: prev.refStack.slice(0, prev.refStack.length - 1) })); } - removeByRef (ref) { + cut (ref) { this.setState(prev => { - const refs = prev.openRefs; + const refs = prev.refStack; const index = refs.indexOf(ref); - if (index === -1) return {openRefs: refs}; + if (index === -1) return {refStack: refs}; return { - openRefs: refs.slice(0, index) + refStack: refs.slice(0, index) }; }); } - removeInner () { - this.setState(prev => ({ - openRefs: prev.openRefs.slice(0, prev.openRefs.length - 1) - })); + clear () { + this.setState({refStack: []}); } - removeAll () { - this.setState({openRefs: []}); + isTopMenu (ref) { + const {refStack} = this.state; + return refStack.length > 0 && refStack[refStack.length - 1] === ref; + } + + isOpenMenu (ref) { + return this.state.refStack.includes(ref); } render () { const value = { - openRefs: this.state.openRefs, + refStack: this.state.refStack, + push: this.push, + pop: this.pop, + cut: this.cut, + clear: this.clear, isTopMenu: this.isTopMenu, - isOpenMenu: this.isOpenMenu, - addInner: this.addInner, - removeInner: this.removeInner, - removeAll: this.removeAll, - removeByRef: this.removeByRef + isOpenMenu: this.isOpenMenu }; return ( diff --git a/packages/scratch-gui/src/components/menu-bar/base-menu.jsx b/packages/scratch-gui/src/components/menu-bar/base-menu.jsx new file mode 100644 index 0000000000..6e79ba6e1a --- /dev/null +++ b/packages/scratch-gui/src/components/menu-bar/base-menu.jsx @@ -0,0 +1,97 @@ +import {MenuRefContext} from '../context-menu/menu-path-context'; +import React from 'react'; +import bindAll from 'lodash.bindall'; +import PropTypes from 'prop-types'; + +// Subclasses must implement render, onSelectItem and define this.itemRefs and this.state.depth +export class BaseMenu extends React.PureComponent { + constructor (props) { + super(props); + bindAll(this, [ + 'onSelectItem', + 'handleKeyPress', + 'handleKeyPressOpenMenu', + 'handleMove', + 'handleOnOpen', + 'handleOnClose', + 'setFocusedRef' + ]); + + this.state = {focusedIndex: -1, depth: -1}; + this.focusedRef = props.focusedRef || React.createRef(); + } + + static contextType = MenuRefContext; + + setFocusedRef (ref) { + this.focusedRef = ref; + if (ref && ref.current) ref.current.focus(); + } + + handleKeyPress (e) { + if (this.context.isTopMenu(this.props.focusedRef)) { + this.handleKeyPressOpenMenu(e); + } else if (!this.context.isOpenMenu(this.props.focusedRef) && (e.key === ' ' || e.key === 'ArrowRight')) { + e.preventDefault(); + this.handleOnOpen(); + } + } + + handleKeyPressOpenMenu (e) { + if (e.key === 'ArrowDown') { + e.preventDefault(); + this.handleMove(1); + } + if (e.key === 'ArrowUp') { + e.preventDefault(); + this.handleMove(-1); + } + if (e.key === 'Enter') { + e.preventDefault(); + this.onSelectItem(); + } + if (e.key === 'ArrowLeft' || e.key === 'Escape') { + e.preventDefault(); + this.handleOnClose(); + } + } + + handleOnOpen () { + if (this.context.isOpenMenu(this.props.focusedRef)) return; + + this.props.onOpen(); + this.setState({focusedIndex: 0}, () => { + if (this.itemRefs[0] && this.itemRefs[0].current) this.itemRefs[0].current.focus(); + }); + + this.context.push(this.props.focusedRef, this.props.depth); + } + + handleMove (direction) { + const newIndex = (this.state.focusedIndex + direction + this.itemRefs.length) % this.itemRefs.length; + this.setState({focusedIndex: newIndex}, () => { + this.setFocusedRef(this.itemRefs[newIndex]); + }); + } + + onSelectItem () { + // do nothing by default, change for items that don't expand + } + + handleOnClose () { + this.context.cut(this.props.focusedRef); + this.setState({focusedIndex: -1}, () => { + this.setFocusedRef(this.props.focusedRef); + }); + + this.props.onClose(); + } + +} + +BaseMenu.propTypes = { + focusedRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), + depth: PropTypes.number, + onOpen: PropTypes.func, + onClose: PropTypes.func +}; diff --git a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx index 2de5339149..927fec2ea7 100644 --- a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx @@ -16,22 +16,17 @@ import {MenuRefContext} from '../context-menu/menu-path-context.jsx'; import styles from './settings-menu.css'; import dropdownCaret from './dropdown-caret.svg'; +import {BaseMenu} from './base-menu'; -class LanguageMenu extends React.PureComponent { +class LanguageMenu extends BaseMenu { constructor (props) { super(props); bindAll(this, [ - 'handleKeyPress', - 'handleKeyPressOpenMenu', - 'handleMove', - 'handleOnOpen', - 'handleOnClose', - 'setFocusedRef', + 'onSelectItem', 'setRef', 'handleMouseOver' ]); - this.state = {focusedIndex: -1}; this.itemRefs = Object.keys(locales).map(() => React.createRef()); } @@ -48,43 +43,9 @@ class LanguageMenu extends React.PureComponent { this.selectedRef = component; } - handleKeyPress (e) { - if (this.context.isTopMenu(this.props.focusedRef)) { - this.handleKeyPressOpenMenu(e); - } else if (!this.context.isOpenMenu(this.props.focusedRef) && (e.key === ' ' || e.key === 'ArrowRight')) { - e.preventDefault(); - this.handleOnOpen(); - } - } - - handleKeyPressOpenMenu (e) { - if (e.key === 'ArrowDown') { - e.preventDefault(); - this.handleMove(1); - } - if (e.key === 'ArrowUp') { - e.preventDefault(); - this.handleMove(-1); - } - - if (e.key === 'Enter') { - e.preventDefault(); - this.props.onChangeLanguage(Object.keys(locales)[this.state.focusedIndex]); - this.handleOnClose(); - } - - if (e.key === 'ArrowLeft' || e.key === 'Escape') { - e.preventDefault(); - this.handleOnClose(); - } - } - - handleMove (move) { - const newIndex = (this.state.focusedIndex + move + this.itemRefs.length) % this.itemRefs.length; - this.setState({focusedIndex: newIndex}, () => { - const ref = this.itemRefs[this.state.focusedIndex]; - if (ref && ref.current) ref.current.focus(); - }); + onSelectItem () { + this.props.onChangeLanguage(Object.keys(locales)[this.state.focusedIndex]); + this.context.clear(); } handleMouseOver () { @@ -95,32 +56,6 @@ class LanguageMenu extends React.PureComponent { } } - handleOnOpen () { - if (this.context.isOpenMenu(this.props.focusedRef)) return; - - this.props.onRequestOpen(); - this.setState({focusedIndex: Object.keys(locales).indexOf(this.props.currentLocale)}, () => { - this.setFocusedRef(this.itemRefs[this.state.focusedIndex]); - }); - - this.context.addInner(this.props.focusedRef); - } - - handleOnClose () { - this.context.removeByRef(this.props.focusedRef); - this.setState({focusedIndex: -1}, () => { - this.setFocusedRef(this.props.focusedRef); - }); - closeLanguageMenu(); - } - - setFocusedRef (component) { - this.focusedRef = component; - if (this.focusedRef && this.focusedRef.current) { - this.focusedRef.current.focus(); - } - } - render () { const { currentLocale, @@ -197,9 +132,8 @@ LanguageMenu.propTypes = { isRtl: PropTypes.bool, menuOpen: PropTypes.bool, onChangeLanguage: PropTypes.func, - onRequestCloseSettings: PropTypes.func, - onRequestOpen: PropTypes.func, - onRequestClose: PropTypes.func + onOpen: PropTypes.func, + onClose: PropTypes.func }; const mapStateToProps = state => ({ @@ -209,13 +143,12 @@ const mapStateToProps = state => ({ messagesByLocale: state.locales.messagesByLocale }); -const mapDispatchToProps = (dispatch, ownProps) => ({ +const mapDispatchToProps = dispatch => ({ onChangeLanguage: locale => { dispatch(selectLocale(locale)); - ownProps.onRequestCloseSettings(); }, - onRequestOpen: () => dispatch(openLanguageMenu()), - onRequestClose: () => dispatch(closeLanguageMenu()) + onOpen: () => dispatch(openLanguageMenu()), + onClose: () => dispatch(closeLanguageMenu()) }); export default connect( diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx index b540408896..5e3d8f0719 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx @@ -15,21 +15,14 @@ import styles from './settings-menu.css'; import dropdownCaret from './dropdown-caret.svg'; import settingsIcon from './icon--settings.svg'; +import {BaseMenu} from './base-menu.jsx'; -class SettingsMenu extends React.Component { +class SettingsMenu extends BaseMenu { constructor (props) { super(props); - bindAll(this, [ - 'handleOnClose', - 'handleKeyPress', - 'handleKeyPressOpenMenu', - 'handleMove', - 'handleOnOpen', - 'setFocusedRef' - ]); + bindAll(this, ['handleKeyPress']); - this.settingsRef = React.createRef(); this.state = {focusedIndex: -1}; this.languageRef = React.createRef(); this.themeRef = React.createRef(); @@ -37,75 +30,14 @@ class SettingsMenu extends React.Component { this.itemRefs = [this.languageRef, this.themeRef]; } - componentDidUpdate (prevProps) { - if (!prevProps.settingsMenuOpen && this.props.settingsMenuOpen) { - this.setState({focusedIndex: 0}, () => { - this.setFocusedRef(this.itemRefs[0]); - }); - } - } - static contextType = MenuRefContext; - handleOnClose () { - this.context.removeByRef(this.settingsRef); - this.props.onRequestClose(); - this.setState({focusedIndex: -1}); - } - - handleOnOpen () { - if (this.context.isOpenMenu(this.settingsRef)) return; - - this.setState({focusedIndex: 0}, () => { - this.props.onRequestOpen(); - this.context.addInner(this.settingsRef); - this.setFocusedRef(this.itemRefs[0]); - }); - } - - setFocusedRef (component) { - this.focusedRef = component; - if (this.focusedRef && this.focusedRef.current) { - this.focusedRef.current.focus(); - } - } - handleKeyPress (e) { if (e.key === 'Tab') { this.handleOnClose(); } - if (this.context.isTopMenu(this.settingsRef)) { - this.handleKeyPressOpenMenu(e); - } else if (!this.context.isOpenMenu(this.settingsRef) && (e.key === ' ' || e.key === 'ArrowRight')) { - e.preventDefault(); - this.handleOnOpen(); - } - } - - handleKeyPressOpenMenu (e) { - if (e.key === 'ArrowLeft' || e.key === 'Escape') { - e.preventDefault(); - this.handleOnClose(); - } - - if (e.key === 'ArrowUp') { - e.preventDefault(); - this.handleMove(-1); - } - - if (e.key === 'ArrowDown') { - e.preventDefault(); - this.handleMove(1); - } - } - - handleMove (direction) { - const nextIndex = - (this.state.focusedIndex + direction + this.itemRefs.length) % this.itemRefs.length; - this.setState({focusedIndex: nextIndex}, () => { - this.setFocusedRef(this.itemRefs[nextIndex]); - }); + super.handleKeyPress(e); } render () { @@ -113,7 +45,6 @@ class SettingsMenu extends React.Component { canChangeLanguage, canChangeTheme, isRtl, - onRequestClose, settingsMenuOpen } = this.props; @@ -142,16 +73,16 @@ class SettingsMenu extends React.Component { className={menuBarStyles.menuBarMenu} open={this.context.isOpenMenu(this.settingsRef)} place={isRtl ? 'left' : 'right'} - onRequestClose={this.handleOnClose} + onClose={this.handleOnClose} > {canChangeLanguage && } {canChangeTheme && } @@ -163,8 +94,8 @@ SettingsMenu.propTypes = { canChangeLanguage: PropTypes.bool, canChangeTheme: PropTypes.bool, isRtl: PropTypes.bool, - onRequestClose: PropTypes.func, - onRequestOpen: PropTypes.func, + onClose: PropTypes.func, + onOpen: PropTypes.func, settingsMenuOpen: PropTypes.bool }; diff --git a/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx b/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx index 0cddd1d3ad..0291b6a030 100644 --- a/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx @@ -16,6 +16,7 @@ import {MenuRefContext} from '../context-menu/menu-path-context.jsx'; import styles from './settings-menu.css'; import dropdownCaret from './dropdown-caret.svg'; +import {BaseMenu} from './base-menu'; const ThemeMenuItem = props => { const themeInfo = themeMap[props.theme]; @@ -48,20 +49,14 @@ ThemeMenuItem.propTypes = { onParentKeyPress: PropTypes.func }; -class ThemeMenu extends React.PureComponent { +class ThemeMenu extends BaseMenu { constructor (props) { super(props); bindAll(this, [ - 'handleKeyPress', - 'handleKeyPressOpenMenu', - 'handleMove', - 'handleOnOpen', - 'handleOnClose', - 'setFocusedRef', - 'setRef' + 'setRef', + 'onSelectItem' ]); - this.state = {focusedIndex: -1}; this.enabledThemes = [DEFAULT_THEME, HIGH_CONTRAST_THEME]; this.itemRefs = this.enabledThemes.map(() => React.createRef()); } @@ -72,69 +67,9 @@ class ThemeMenu extends React.PureComponent { this.selectedRef = component; } - handleKeyPress (e) { - if (this.context.isTopMenu(this.props.focusedRef)) { - this.handleKeyPressOpenMenu(e); - } else if (!this.context.isOpenMenu(this.props.focusedRef) && (e.key === ' ' || e.key === 'ArrowRight')) { - e.preventDefault(); - this.handleOnOpen(); - } - } - - handleKeyPressOpenMenu (e) { - if (e.key === 'ArrowDown') { - e.preventDefault(); - this.handleMove(1); - } - if (e.key === 'ArrowUp') { - e.preventDefault(); - this.handleMove(-1); - } - - if (e.key === 'Enter') { - e.preventDefault(); - this.props.onChangeTheme(this.enabledThemes[this.state.focusedIndex]); - this.handleOnClose(); - } - - if (e.key === 'ArrowLeft' || e.key === 'Escape') { - e.preventDefault(); - this.handleOnClose(); - } - } - - handleMove (move) { - const newIndex = (this.state.focusedIndex + move + this.itemRefs.length) % this.itemRefs.length; - this.setState({focusedIndex: newIndex}, () => { - const ref = this.itemRefs[this.state.focusedIndex]; - if (ref && ref.current) ref.current.focus(); - }); - } - - handleOnOpen () { - if (this.context.isTopMenu(this.props.focusedRef)) return; - - this.props.onRequestOpen(); - this.setState({focusedIndex: 0}, () => { - this.setFocusedRef(this.itemRefs[this.state.focusedIndex]); - }); - - this.context.addInner(this.props.focusedRef); - } - - handleOnClose () { - this.context.removeByRef(this.props.focusedRef); - this.setState({focusedIndex: -1}, () => { - this.setFocusedRef(this.props.focusedRef); - }); - this.props.onRequestClose(); - } - - setFocusedRef (component) { - this.focusedRef = component; - if (this.focusedRef && this.focusedRef.current) { - this.focusedRef.current.focus(); - } + onSelectItem () { + this.props.onChangeTheme(this.enabledThemes[this.state.focusedIndex]); + this.context.clear(); } render () { @@ -198,10 +133,8 @@ ThemeMenu.propTypes = { focusedRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), isRtl: PropTypes.bool, onChangeTheme: PropTypes.func, - // eslint-disable-next-line react/no-unused-prop-types - onRequestCloseSettings: PropTypes.func, - onRequestOpen: PropTypes.func, - onRequestClose: PropTypes.func, + onOpen: PropTypes.func, + onClose: PropTypes.func, theme: PropTypes.string }; @@ -210,14 +143,13 @@ const mapStateToProps = state => ({ theme: state.scratchGui.theme.theme }); -const mapDispatchToProps = (dispatch, ownProps) => ({ +const mapDispatchToProps = dispatch => ({ onChangeTheme: theme => { dispatch(setTheme(theme)); - ownProps.onRequestCloseSettings(); persistTheme(theme); }, - onRequestOpen: () => dispatch(openThemeMenu()), - onRequestClose: () => dispatch(closeThemeMenu()) + onOpen: () => dispatch(openThemeMenu()), + onClose: () => dispatch(closeThemeMenu()) }); export default connect( From 9914e61d9273879446fed4af303cd70f5a7d3a58 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Tue, 23 Dec 2025 18:00:14 +0200 Subject: [PATCH 05/66] feat: more dropdowns in new files and stuff --- .../src/components/menu-bar/base-menu.jsx | 26 ++- .../src/components/menu-bar/edit-menu.jsx | 104 ++++++++++ .../src/components/menu-bar/file-menu.jsx | 181 +++++++++++++++++ .../src/components/menu-bar/menu-bar.jsx | 192 ++++-------------- .../src/components/menu-bar/mode-menu.jsx | 0 .../src/components/menu-bar/settings-menu.jsx | 20 +- .../scratch-gui/src/components/menu/menu.jsx | 2 +- packages/scratch-gui/src/containers/gui.jsx | 3 +- 8 files changed, 353 insertions(+), 175 deletions(-) create mode 100644 packages/scratch-gui/src/components/menu-bar/edit-menu.jsx create mode 100644 packages/scratch-gui/src/components/menu-bar/file-menu.jsx create mode 100644 packages/scratch-gui/src/components/menu-bar/mode-menu.jsx diff --git a/packages/scratch-gui/src/components/menu-bar/base-menu.jsx b/packages/scratch-gui/src/components/menu-bar/base-menu.jsx index 6e79ba6e1a..d28458dc81 100644 --- a/packages/scratch-gui/src/components/menu-bar/base-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/base-menu.jsx @@ -3,7 +3,21 @@ import React from 'react'; import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; -// Subclasses must implement render, onSelectItem and define this.itemRefs and this.state.depth +/* Subclasses must implement (some optionally): +_______________________________________________ +render +onSelectItem +define this.itemRefs +add onKeyDown={this.handleKeyPress} +and onParentKeyPress={this.handleKeyPress} for MenuItem elements + +They should also receive: +______________________ +onOpen, +onClose, +focusedRef, +depth +*/ export class BaseMenu extends React.PureComponent { constructor (props) { super(props); @@ -29,6 +43,12 @@ export class BaseMenu extends React.PureComponent { } handleKeyPress (e) { + if (this.props.depth === 1) { + if (e.key === 'Tab') { + this.handleOnClose(); + } + } + if (this.context.isTopMenu(this.props.focusedRef)) { this.handleKeyPressOpenMenu(e); } else if (!this.context.isOpenMenu(this.props.focusedRef) && (e.key === ' ' || e.key === 'ArrowRight')) { @@ -95,3 +115,7 @@ BaseMenu.propTypes = { onOpen: PropTypes.func, onClose: PropTypes.func }; + +BaseMenu.defaultProps = { + onClose: () => {} +}; diff --git a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx new file mode 100644 index 0000000000..de47471b83 --- /dev/null +++ b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx @@ -0,0 +1,104 @@ +import React from 'react'; +import styles from './menu-bar.css'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import bindAll from 'lodash.bindall'; + +import editIcon from './icon--edit.svg'; +import {FormattedMessage} from 'react-intl'; +import MenuBarMenu from './menu-bar-menu.jsx'; +import {MenuItem, MenuSection} from '../menu/menu.jsx'; +import {BaseMenu} from './base-menu'; +import dropdownCaret from './dropdown-caret.svg'; +import DeletionRestorer from '../../containers/deletion-restorer.jsx'; +import TurboMode from '../../containers/turbo-mode.jsx'; + +export class EditMenu extends BaseMenu { + constructor (props) { + super(props); + + bindAll(this, ['handleOnClose', 'handleKeyPress', 'handleOnOpen']); + + this.state = {focusedIndex: -1}; + + this.restoreRef = React.createRef(); + this.turboRef = React.createRef(); + + this.itemRefs = [ + this.restoreRef, + this.turboRef + ]; + } + + render () { + return ( +
    + + + + + + + {(handleRestore, {restorable, deletedItem}) => ( + + {this.props.restoreOptionMessage(deletedItem)} + + )} + + {(toggleTurboMode, {turboMode}) => ( + + {turboMode ? ( + + ) : ( + + )} + + )} + + +
    + ); + } +} + +EditMenu.propTypes = { + focusedRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), + isRtl: PropTypes.bool, + restoreOptionMessage: PropTypes.func, + onRestoreOption: PropTypes.func +}; diff --git a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx new file mode 100644 index 0000000000..195c56d845 --- /dev/null +++ b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx @@ -0,0 +1,181 @@ +import React from 'react'; +import styles from './menu-bar.css'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import bindAll from 'lodash.bindall'; + +import fileIcon from './icon--file.svg'; +import {FormattedMessage} from 'react-intl'; +import MenuBarMenu from './menu-bar-menu.jsx'; +import {MenuItem, MenuSection} from '../menu/menu.jsx'; +import {BaseMenu} from './base-menu'; +import SB3Downloader from '../../containers/sb3-downloader.jsx'; +import dropdownCaret from './dropdown-caret.svg'; + +import sharedMessages from '../../lib/shared-messages'; +import intlShape from '../../lib/intlShape.js'; + +export class FileMenu extends BaseMenu { + constructor (props) { + super(props); + + bindAll(this, ['handleOnClose', 'handleKeyPress', 'handleOnOpen']); + + this.state = {focusedIndex: -1}; + + this.newProjectRef = React.createRef(); + this.saveRef = React.createRef(); + this.createRef = React.createRef(); + this.remixRef = React.createRef(); + this.loadFromComputerRef = React.createRef(); + this.saveToComputerRef = React.createRef(); + + this.itemRefs = [ + this.newProjectRef, + ...(this.props.canSave ? [this.saveRef] : []), + ...(this.props.canCreateCopy ? [this.createRef] : []), + ...(this.props.canRemix ? [this.remixRef] : []), + this.loadFromComputerRef, + this.saveToComputerRef + ]; + } + + render () { + const saveNowMessage = ( + + ); + const createCopyMessage = ( + + ); + const remixMessage = ( + + ); + const newProjectMessage = ( + + ); + return ( +
    + + + + + + + + + {newProjectMessage} + + + {(this.props.canSave || this.props.canCreateCopy || this.props.canRemix) && ( + + {this.props.canSave && ( + + {saveNowMessage} + + )} + {this.props.canCreateCopy && ( + + {createCopyMessage} + + )} + {this.props.canRemix && ( + + {remixMessage} + + )} + + )} + + + {this.props.intl.formatMessage(sharedMessages.loadFromComputerTitle)} + + {(className, downloadProjectCallback) => ( + + + + )} + + +
    + ); + } +} + +FileMenu.propTypes = { + focusedRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), + intl: intlShape, + isRtl: PropTypes.bool, + canSave: PropTypes.bool, + canCreateCopy: PropTypes.bool, + canRemix: PropTypes.bool, + onStartSelectingFileUpload: PropTypes.func, + onClickSave: PropTypes.func, + onClickSaveAsCopy: PropTypes.func, + onClickRemix: PropTypes.func, + onClickNew: PropTypes.func +}; diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx index 180a818d9b..5c95483f5d 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx @@ -24,11 +24,10 @@ import ProjectTitleInput from './project-title-input.jsx'; import AuthorInfo from './author-info.jsx'; import AccountNav from '../../components/menu-bar/account-nav.jsx'; import LoginDropdown from './login-dropdown.jsx'; -import SB3Downloader from '../../containers/sb3-downloader.jsx'; -import DeletionRestorer from '../../containers/deletion-restorer.jsx'; -import TurboMode from '../../containers/turbo-mode.jsx'; import MenuBarHOC from '../../containers/menu-bar-hoc.jsx'; import SettingsMenu from './settings-menu.jsx'; +import {FileMenu} from './file-menu.jsx'; +import {EditMenu} from './edit-menu.jsx'; import {openTipsLibrary, openDebugModal} from '../../reducers/modals'; import {setPlayer} from '../../reducers/mode'; @@ -84,8 +83,6 @@ import profileIcon from './icon--profile.png'; import remixIcon from './icon--remix.svg'; import dropdownCaret from './dropdown-caret.svg'; import aboutIcon from './icon--about.svg'; -import fileIcon from './icon--file.svg'; -import editIcon from './icon--edit.svg'; import debugIcon from '../debug-modal/icons/icon--debug.svg'; import scratchLogo from './scratch-logo.svg'; @@ -197,6 +194,10 @@ class MenuBar extends React.Component { 'getSaveToComputerHandler', 'restoreOptionMessage' ]); + + this.settingsRef = React.createRef(); + this.fileRef = React.createRef(); + this.editRef = React.createRef(); } componentDidMount () { document.addEventListener('keydown', this.handleKeyPress); @@ -390,20 +391,6 @@ class MenuBar extends React.Component { }; } render () { - const saveNowMessage = ( - - ); - const createCopyMessage = ( - - ); const remixMessage = ( ); - const newProjectMessage = ( - - ); const remixButton = (
    {(this.props.canChangeTheme || this.props.canChangeLanguage) && ()} - {(this.props.canManageFiles) && ( -
    - - - - - - - - - {newProjectMessage} - - - {(this.props.canSave || this.props.canCreateCopy || this.props.canRemix) && ( - - {this.props.canSave && ( - - {saveNowMessage} - - )} - {this.props.canCreateCopy && ( - - {createCopyMessage} - - )} - {this.props.canRemix && ( - - {remixMessage} - - )} - - )} - - - {this.props.intl.formatMessage(sharedMessages.loadFromComputerTitle)} - - {(className, downloadProjectCallback) => ( - - - - )} - - -
    - )} -
    - - - - - - - {(handleRestore, {restorable, deletedItem}) => ( - - {this.restoreOptionMessage(deletedItem)} - - )} - - {(toggleTurboMode, {turboMode}) => ( - - {turboMode ? ( - - ) : ( - - )} - - )} - - - -
    + {(this.props.canManageFiles) && ()} + {this.props.isTotallyNormal && (
    @@ -71,7 +63,7 @@ class SettingsMenu extends BaseMenu { @@ -94,8 +86,6 @@ SettingsMenu.propTypes = { canChangeLanguage: PropTypes.bool, canChangeTheme: PropTypes.bool, isRtl: PropTypes.bool, - onClose: PropTypes.func, - onOpen: PropTypes.func, settingsMenuOpen: PropTypes.bool }; diff --git a/packages/scratch-gui/src/components/menu/menu.jsx b/packages/scratch-gui/src/components/menu/menu.jsx index 523d6946c9..972edd3510 100644 --- a/packages/scratch-gui/src/components/menu/menu.jsx +++ b/packages/scratch-gui/src/components/menu/menu.jsx @@ -87,13 +87,13 @@ const MenuItem = ({ ); MenuItem.propTypes = { + focusedRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), ariaLabel: PropTypes.string, ariaRole: PropTypes.string, children: PropTypes.node, className: PropTypes.string, expanded: PropTypes.bool, onClick: PropTypes.func, - focusedRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), onParentKeyPress: PropTypes.func }; diff --git a/packages/scratch-gui/src/containers/gui.jsx b/packages/scratch-gui/src/containers/gui.jsx index 375790d47b..1890a6c6f1 100644 --- a/packages/scratch-gui/src/containers/gui.jsx +++ b/packages/scratch-gui/src/containers/gui.jsx @@ -148,8 +148,7 @@ GUI.propTypes = { }; GUI.defaultProps = { - // isTotallyNormal: true, - for testing only - isTotallyNormal: false, + isTotallyNormal: true, onStorageInit: () => {}, onProjectLoaded: () => {}, onUpdateProjectId: () => {}, From 328b234e5a66dd560b23ce07d61923c6c77213b1 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 29 Dec 2025 10:43:23 +0200 Subject: [PATCH 06/66] chore: completed functionality and refactored code inconsistencies --- .../context-menu/menu-path-context.jsx | 10 +- .../src/components/menu-bar/base-menu.jsx | 65 +++++++----- .../src/components/menu-bar/edit-menu.jsx | 20 ++-- .../src/components/menu-bar/file-menu.jsx | 36 +++---- .../src/components/menu-bar/language-menu.jsx | 25 ++--- .../src/components/menu-bar/menu-bar.jsx | 75 ++++---------- .../src/components/menu-bar/mode-menu.jsx | 99 +++++++++++++++++++ .../src/components/menu-bar/settings-menu.jsx | 24 ++--- .../src/components/menu-bar/theme-menu.jsx | 27 ++--- .../scratch-gui/src/components/menu/menu.jsx | 10 +- 10 files changed, 225 insertions(+), 166 deletions(-) diff --git a/packages/scratch-gui/src/components/context-menu/menu-path-context.jsx b/packages/scratch-gui/src/components/context-menu/menu-path-context.jsx index 7c562428c7..00eeb96a49 100644 --- a/packages/scratch-gui/src/components/context-menu/menu-path-context.jsx +++ b/packages/scratch-gui/src/components/context-menu/menu-path-context.jsx @@ -18,7 +18,8 @@ export class MenuRefProvider extends React.Component { 'cut', 'clear', 'isTopMenu', - 'isOpenMenu' + 'isOpenMenu', + 'print' ]); } @@ -64,6 +65,10 @@ export class MenuRefProvider extends React.Component { return this.state.refStack.includes(ref); } + print () { + console.log(this.state.refStack); + } + render () { const value = { refStack: this.state.refStack, @@ -72,7 +77,8 @@ export class MenuRefProvider extends React.Component { cut: this.cut, clear: this.clear, isTopMenu: this.isTopMenu, - isOpenMenu: this.isOpenMenu + isOpenMenu: this.isOpenMenu, + print: this.print }; return ( diff --git a/packages/scratch-gui/src/components/menu-bar/base-menu.jsx b/packages/scratch-gui/src/components/menu-bar/base-menu.jsx index d28458dc81..080ccc08ef 100644 --- a/packages/scratch-gui/src/components/menu-bar/base-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/base-menu.jsx @@ -6,58 +6,63 @@ import PropTypes from 'prop-types'; /* Subclasses must implement (some optionally): _______________________________________________ render -onSelectItem define this.itemRefs add onKeyDown={this.handleKeyPress} -and onParentKeyPress={this.handleKeyPress} for MenuItem elements +and onParentKeyPress={this.handleKeyPressSubmenu} for MenuItem elements + +and replace isOpenMenu-like props with this.isExpanded() They should also receive: ______________________ onOpen, onClose, -focusedRef, +menuRef, depth */ export class BaseMenu extends React.PureComponent { constructor (props) { super(props); bindAll(this, [ - 'onSelectItem', 'handleKeyPress', 'handleKeyPressOpenMenu', 'handleMove', 'handleOnOpen', 'handleOnClose', - 'setFocusedRef' + 'refocusRef', + 'isExpanded', + 'isInnermostExpanded' ]); this.state = {focusedIndex: -1, depth: -1}; - this.focusedRef = props.focusedRef || React.createRef(); + this.menuRef = props.menuRef; } static contextType = MenuRefContext; - setFocusedRef (ref) { - this.focusedRef = ref; - if (ref && ref.current) ref.current.focus(); + refocusRef (ref) { + if (ref && ref.current) { + ref.current.focus(); + } } handleKeyPress (e) { if (this.props.depth === 1) { if (e.key === 'Tab') { this.handleOnClose(); + this.context.clear(); } } - if (this.context.isTopMenu(this.props.focusedRef)) { + if (this.context.isTopMenu(this.menuRef)) { this.handleKeyPressOpenMenu(e); - } else if (!this.context.isOpenMenu(this.props.focusedRef) && (e.key === ' ' || e.key === 'ArrowRight')) { + } else if (!this.isExpanded() && (e.key === ' ' || e.key === 'ArrowRight')) { e.preventDefault(); this.handleOnOpen(); } } handleKeyPressOpenMenu (e) { + console.log("pressing"); if (e.key === 'ArrowDown') { e.preventDefault(); this.handleMove(1); @@ -66,9 +71,8 @@ export class BaseMenu extends React.PureComponent { e.preventDefault(); this.handleMove(-1); } - if (e.key === 'Enter') { - e.preventDefault(); - this.onSelectItem(); + if (e.key === 'Enter' && this.props.clearOnItemSelect) { + this.context.clear(); } if (e.key === 'ArrowLeft' || e.key === 'Escape') { e.preventDefault(); @@ -77,45 +81,52 @@ export class BaseMenu extends React.PureComponent { } handleOnOpen () { - if (this.context.isOpenMenu(this.props.focusedRef)) return; + if (this.context.isOpenMenu(this.menuRef)) return; this.props.onOpen(); this.setState({focusedIndex: 0}, () => { - if (this.itemRefs[0] && this.itemRefs[0].current) this.itemRefs[0].current.focus(); + this.refocusRef(this.itemRefs[0]); }); - this.context.push(this.props.focusedRef, this.props.depth); + this.context.push(this.menuRef, this.props.depth); } handleMove (direction) { const newIndex = (this.state.focusedIndex + direction + this.itemRefs.length) % this.itemRefs.length; this.setState({focusedIndex: newIndex}, () => { - this.setFocusedRef(this.itemRefs[newIndex]); + this.refocusRef(this.itemRefs[newIndex]); }); - } - - onSelectItem () { - // do nothing by default, change for items that don't expand + console.log(newIndex); + this.context.print(); } handleOnClose () { - this.context.cut(this.props.focusedRef); + this.context.cut(this.menuRef); this.setState({focusedIndex: -1}, () => { - this.setFocusedRef(this.props.focusedRef); + this.refocusRef(this.menuRef); }); this.props.onClose(); } + isExpanded () { + return this.context.isOpenMenu(this.menuRef); + } + + isInnermostExpanded () { + return this.context.isTopMenu(this.menuRef); + } } BaseMenu.propTypes = { - focusedRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), + menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), depth: PropTypes.number, onOpen: PropTypes.func, - onClose: PropTypes.func + onClose: PropTypes.func, + clearOnItemSelect: PropTypes.bool }; BaseMenu.defaultProps = { - onClose: () => {} + onClose: () => {}, + clearOnItemSelect: false }; diff --git a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx index de47471b83..ca96bd12d6 100644 --- a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx @@ -17,9 +17,7 @@ export class EditMenu extends BaseMenu { constructor (props) { super(props); - bindAll(this, ['handleOnClose', 'handleKeyPress', 'handleOnOpen']); - - this.state = {focusedIndex: -1}; + bindAll(this); this.restoreRef = React.createRef(); this.turboRef = React.createRef(); @@ -34,7 +32,7 @@ export class EditMenu extends BaseMenu { return (
    @@ -61,8 +59,8 @@ export class EditMenu extends BaseMenu { {this.props.restoreOptionMessage(deletedItem)} @@ -71,8 +69,8 @@ export class EditMenu extends BaseMenu { {(toggleTurboMode, {turboMode}) => ( {turboMode ? ( @@ -92,7 +90,7 @@ export class FileMenu extends BaseMenu { @@ -100,8 +98,8 @@ export class FileMenu extends BaseMenu { {newProjectMessage} @@ -111,8 +109,8 @@ export class FileMenu extends BaseMenu { {this.props.canSave && ( {saveNowMessage} @@ -120,8 +118,8 @@ export class FileMenu extends BaseMenu { {this.props.canCreateCopy && ( {createCopyMessage} @@ -129,8 +127,8 @@ export class FileMenu extends BaseMenu { {this.props.canRemix && ( {remixMessage} @@ -140,8 +138,8 @@ export class FileMenu extends BaseMenu { {this.props.intl.formatMessage(sharedMessages.loadFromComputerTitle)} @@ -149,8 +147,8 @@ export class FileMenu extends BaseMenu { +
    onChangeLanguage(locale)} - focusedRef={this.itemRefs[index]} - onParentKeyPress={this.handleKeyPress} + menuRef={this.itemRefs[index]} + onParentKeyPress={this.handleKeyPressOpenMenu} + isSelected={isSelected} + // ariaRole="option" >
    {(this.props.canChangeTheme || this.props.canChangeLanguage) && ()} {(this.props.canManageFiles) && ()} - {this.props.isTotallyNormal && ( -
    -
    - -
    - - - - - {'✓'} - - {' '} - - - - - {'✓'} - - {' '} - - - - -
    - )} + {this.props.isTotallyNormal && ()}
    {this.props.canEditTitle ? (
    diff --git a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx index e69de29bb2..afac6639db 100644 --- a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx @@ -0,0 +1,99 @@ +import React from 'react'; +import styles from './menu-bar.css'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import bindAll from 'lodash.bindall'; + +import {FormattedMessage} from 'react-intl'; +import MenuBarMenu from './menu-bar-menu.jsx'; +import {MenuItem, MenuSection} from '../menu/menu.jsx'; +import {BaseMenu} from './base-menu'; + +export class ModeMenu extends BaseMenu { + constructor (props) { + super(props); + + bindAll(this); + + this.normalRef = React.createRef(); + this.caturdayRef = React.createRef(); + + this.itemRefs = [ + this.normalRef, + this.caturdayRef + ]; + } + + render () { + return ( +
    +
    + +
    + + + + + {'✓'} + + {' '} + + + + + {'✓'} + + {' '} + + + + +
    + ); + } +} + +ModeMenu.propTypes = { + menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), + onSetMode: PropTypes.func, + modeNow: PropTypes.bool, + mode2020: PropTypes.bool, + isRtl: PropTypes.bool + +}; + +export default ModeMenu; diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx index ebb8d26c30..32b4227b55 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx @@ -8,7 +8,6 @@ import LanguageMenu from './language-menu.jsx'; import MenuBarMenu from './menu-bar-menu.jsx'; import ThemeMenu from './theme-menu.jsx'; import {MenuSection} from '../menu/menu.jsx'; -import {MenuRefContext} from '../context-menu/menu-path-context.jsx'; import menuBarStyles from './menu-bar.css'; import styles from './settings-menu.css'; @@ -22,8 +21,6 @@ class SettingsMenu extends BaseMenu { super(props); bindAll(this, []); - - this.state = {focusedIndex: -1}; this.languageRef = React.createRef(); this.themeRef = React.createRef(); @@ -31,26 +28,24 @@ class SettingsMenu extends BaseMenu { this.itemRefs = [this.languageRef, this.themeRef]; } - static contextType = MenuRefContext; render () { const { canChangeLanguage, canChangeTheme, - isRtl, - settingsMenuOpen + isRtl } = this.props; return (
    @@ -63,17 +58,17 @@ class SettingsMenu extends BaseMenu { {canChangeLanguage && } {canChangeTheme && } @@ -85,8 +80,7 @@ class SettingsMenu extends BaseMenu { SettingsMenu.propTypes = { canChangeLanguage: PropTypes.bool, canChangeTheme: PropTypes.bool, - isRtl: PropTypes.bool, - settingsMenuOpen: PropTypes.bool + isRtl: PropTypes.bool }; export default SettingsMenu; diff --git a/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx b/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx index 0291b6a030..9ae95df21f 100644 --- a/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx @@ -11,7 +11,6 @@ import {DEFAULT_THEME, HIGH_CONTRAST_THEME, themeMap} from '../../lib/themes'; import {persistTheme} from '../../lib/themes/themePersistance'; import {openThemeMenu, closeThemeMenu} from '../../reducers/menus.js'; import {setTheme} from '../../reducers/theme.js'; -import {MenuRefContext} from '../context-menu/menu-path-context.jsx'; import styles from './settings-menu.css'; @@ -24,7 +23,7 @@ const ThemeMenuItem = props => { return (
    @@ -45,7 +44,7 @@ ThemeMenuItem.propTypes = { isSelected: PropTypes.bool, onClick: PropTypes.func, theme: PropTypes.string, - focusedRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), + menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), onParentKeyPress: PropTypes.func }; @@ -53,28 +52,20 @@ class ThemeMenu extends BaseMenu { constructor (props) { super(props); bindAll(this, [ - 'setRef', - 'onSelectItem' + 'setRef' ]); this.enabledThemes = [DEFAULT_THEME, HIGH_CONTRAST_THEME]; this.itemRefs = this.enabledThemes.map(() => React.createRef()); } - - static contextType = MenuRefContext; setRef (component) { this.selectedRef = component; } - onSelectItem () { - this.props.onChangeTheme(this.enabledThemes[this.state.focusedIndex]); - this.context.clear(); - } - render () { const { - focusedRef, + menuRef, isRtl, onChangeTheme, theme @@ -84,12 +75,12 @@ class ThemeMenu extends BaseMenu { return (
    onChangeTheme(enabledTheme)} theme={enabledTheme} - focusedRef={this.itemRefs[index]} - onParentKeyPress={this.handleKeyPress} + menuRef={this.itemRefs[index]} + onParentKeyPress={this.handleKeyPressOpenMenu} />) )} @@ -130,7 +121,7 @@ class ThemeMenu extends BaseMenu { } ThemeMenu.propTypes = { - focusedRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), + menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), isRtl: PropTypes.bool, onChangeTheme: PropTypes.func, onOpen: PropTypes.func, diff --git a/packages/scratch-gui/src/components/menu/menu.jsx b/packages/scratch-gui/src/components/menu/menu.jsx index 972edd3510..529f91cf96 100644 --- a/packages/scratch-gui/src/components/menu/menu.jsx +++ b/packages/scratch-gui/src/components/menu/menu.jsx @@ -20,6 +20,7 @@ const MenuComponent = ({ } )} ref={componentRef} + // role="listbox" > {children} @@ -62,8 +63,9 @@ const MenuItem = ({ children, className, expanded = false, + isSelected = false, onClick, - focusedRef, + menuRef, ariaLabel, ariaRole, onParentKeyPress @@ -77,8 +79,9 @@ const MenuItem = ({ )} onClick={onClick} tabIndex={-1} - ref={focusedRef} + ref={menuRef} aria-label={ariaLabel} + // aria-selected={ariaRole === 'option' ? isSelected : null} role={ariaRole} onKeyDown={onParentKeyPress} > @@ -87,12 +90,13 @@ const MenuItem = ({ ); MenuItem.propTypes = { - focusedRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), + menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), ariaLabel: PropTypes.string, ariaRole: PropTypes.string, children: PropTypes.node, className: PropTypes.string, expanded: PropTypes.bool, + isSelected: PropTypes.bool, onClick: PropTypes.func, onParentKeyPress: PropTypes.func }; From 0c20327b98a245ba28ec34d44fc159ceb3f4207f Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 29 Dec 2025 11:36:32 +0200 Subject: [PATCH 07/66] chore: added aria-expanded everywhere on dropdowns --- packages/scratch-gui/src/components/menu-bar/edit-menu.jsx | 1 + packages/scratch-gui/src/components/menu-bar/file-menu.jsx | 1 + packages/scratch-gui/src/components/menu-bar/language-menu.jsx | 2 +- packages/scratch-gui/src/components/menu-bar/mode-menu.jsx | 1 + packages/scratch-gui/src/components/menu-bar/theme-menu.jsx | 1 + packages/scratch-gui/src/components/menu/menu.jsx | 3 +-- 6 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx index ca96bd12d6..4e283170ac 100644 --- a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx @@ -37,6 +37,7 @@ export class EditMenu extends BaseMenu { onClick={this.handleOnOpen} role="button" aria-label="Edit Menu" + aria-expanded={this.isExpanded()} tabIndex={0} onKeyDown={this.handleKeyPress} > diff --git a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx index f2c9b3db02..4b4c33d3a5 100644 --- a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx @@ -74,6 +74,7 @@ export class FileMenu extends BaseMenu { })} onClick={this.handleOnOpen} aria-label="File Menu" + aria-expanded={this.isExpanded()} role="button" tabIndex={0} ref={this.props.menuRef} diff --git a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx index 469d389d62..e4c1a5f551 100644 --- a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx @@ -63,6 +63,7 @@ class LanguageMenu extends BaseMenu { onMouseOver={this.handleMouseOver} ref={menuRef} aria-label="Language Menu" + aria-expanded={this.isExpanded()} role="button" tabIndex={-1} onKeyDown={this.handleKeyPress} @@ -100,7 +101,6 @@ class LanguageMenu extends BaseMenu { menuRef={this.itemRefs[index]} onParentKeyPress={this.handleKeyPressOpenMenu} isSelected={isSelected} - // ariaRole="option" > diff --git a/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx b/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx index 9ae95df21f..0d0522984f 100644 --- a/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx @@ -83,6 +83,7 @@ class ThemeMenu extends BaseMenu { ref={menuRef} role="button" aria-label="Theme Menu" + aria-expanded={this.isExpanded()} tabIndex={-1} onKeyDown={this.handleKeyPress} > diff --git a/packages/scratch-gui/src/components/menu/menu.jsx b/packages/scratch-gui/src/components/menu/menu.jsx index 529f91cf96..8e62587343 100644 --- a/packages/scratch-gui/src/components/menu/menu.jsx +++ b/packages/scratch-gui/src/components/menu/menu.jsx @@ -20,7 +20,6 @@ const MenuComponent = ({ } )} ref={componentRef} - // role="listbox" > {children} @@ -81,7 +80,7 @@ const MenuItem = ({ tabIndex={-1} ref={menuRef} aria-label={ariaLabel} - // aria-selected={ariaRole === 'option' ? isSelected : null} + aria-selected={isSelected ?? null} role={ariaRole} onKeyDown={onParentKeyPress} > From 6f3a80de301e27ec899ad79d26cf8e27cef637ae Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 29 Dec 2025 13:11:44 +0200 Subject: [PATCH 08/66] chore: code redacting --- ...-path-context.jsx => menu-ref-context.jsx} | 10 ++----- .../scratch-gui/src/components/gui/gui.jsx | 2 +- .../src/components/menu-bar/base-menu.jsx | 26 +++++++++---------- .../src/components/menu-bar/edit-menu.jsx | 4 +-- .../src/components/menu-bar/file-menu.jsx | 3 --- .../src/components/menu-bar/language-menu.jsx | 7 ++++- .../src/components/menu-bar/mode-menu.jsx | 3 --- .../src/components/menu-bar/settings-menu.jsx | 3 --- .../scratch-gui/src/components/menu/menu.jsx | 3 +++ packages/scratch-gui/src/containers/gui.jsx | 2 +- 10 files changed, 26 insertions(+), 37 deletions(-) rename packages/scratch-gui/src/components/context-menu/{menu-path-context.jsx => menu-ref-context.jsx} (91%) diff --git a/packages/scratch-gui/src/components/context-menu/menu-path-context.jsx b/packages/scratch-gui/src/components/context-menu/menu-ref-context.jsx similarity index 91% rename from packages/scratch-gui/src/components/context-menu/menu-path-context.jsx rename to packages/scratch-gui/src/components/context-menu/menu-ref-context.jsx index 00eeb96a49..7c562428c7 100644 --- a/packages/scratch-gui/src/components/context-menu/menu-path-context.jsx +++ b/packages/scratch-gui/src/components/context-menu/menu-ref-context.jsx @@ -18,8 +18,7 @@ export class MenuRefProvider extends React.Component { 'cut', 'clear', 'isTopMenu', - 'isOpenMenu', - 'print' + 'isOpenMenu' ]); } @@ -65,10 +64,6 @@ export class MenuRefProvider extends React.Component { return this.state.refStack.includes(ref); } - print () { - console.log(this.state.refStack); - } - render () { const value = { refStack: this.state.refStack, @@ -77,8 +72,7 @@ export class MenuRefProvider extends React.Component { cut: this.cut, clear: this.clear, isTopMenu: this.isTopMenu, - isOpenMenu: this.isOpenMenu, - print: this.print + isOpenMenu: this.isOpenMenu }; return ( diff --git a/packages/scratch-gui/src/components/gui/gui.jsx b/packages/scratch-gui/src/components/gui/gui.jsx index 2f81df9eed..9b120bdbea 100644 --- a/packages/scratch-gui/src/components/gui/gui.jsx +++ b/packages/scratch-gui/src/components/gui/gui.jsx @@ -44,7 +44,7 @@ import soundsIcon from './icon--sounds.svg'; import DebugModal from '../debug-modal/debug-modal.jsx'; import {setPlatform} from '../../reducers/platform.js'; import {PLATFORM} from '../../lib/platform.js'; -import {MenuRefProvider} from '../context-menu/menu-path-context.jsx'; +import {MenuRefProvider} from '../context-menu/menu-ref-context.jsx'; // Cache this value to only retrieve it once the first time. // Assume that it doesn't change for a session. diff --git a/packages/scratch-gui/src/components/menu-bar/base-menu.jsx b/packages/scratch-gui/src/components/menu-bar/base-menu.jsx index 080ccc08ef..b88ae7e3d5 100644 --- a/packages/scratch-gui/src/components/menu-bar/base-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/base-menu.jsx @@ -1,4 +1,4 @@ -import {MenuRefContext} from '../context-menu/menu-path-context'; +import {MenuRefContext} from '../context-menu/menu-ref-context'; import React from 'react'; import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; @@ -10,7 +10,7 @@ define this.itemRefs add onKeyDown={this.handleKeyPress} and onParentKeyPress={this.handleKeyPressSubmenu} for MenuItem elements -and replace isOpenMenu-like props with this.isExpanded() +and replace isOpenMenu-like props with this.isExpanded() checks They should also receive: ______________________ @@ -29,11 +29,11 @@ export class BaseMenu extends React.PureComponent { 'handleOnOpen', 'handleOnClose', 'refocusRef', - 'isExpanded', - 'isInnermostExpanded' + 'refocusItemByIndex', + 'isExpanded' ]); - this.state = {focusedIndex: -1, depth: -1}; + this.state = {focusedIndex: -1}; this.menuRef = props.menuRef; } @@ -45,6 +45,12 @@ export class BaseMenu extends React.PureComponent { } } + refocusItemByIndex (index) { + this.setState({focusedIndex: index}, () => { + this.refocusRef(this.itemRefs[index]); + }); + } + handleKeyPress (e) { if (this.props.depth === 1) { if (e.key === 'Tab') { @@ -62,7 +68,6 @@ export class BaseMenu extends React.PureComponent { } handleKeyPressOpenMenu (e) { - console.log("pressing"); if (e.key === 'ArrowDown') { e.preventDefault(); this.handleMove(1); @@ -84,9 +89,7 @@ export class BaseMenu extends React.PureComponent { if (this.context.isOpenMenu(this.menuRef)) return; this.props.onOpen(); - this.setState({focusedIndex: 0}, () => { - this.refocusRef(this.itemRefs[0]); - }); + this.refocusItemByIndex(0); this.context.push(this.menuRef, this.props.depth); } @@ -96,7 +99,6 @@ export class BaseMenu extends React.PureComponent { this.setState({focusedIndex: newIndex}, () => { this.refocusRef(this.itemRefs[newIndex]); }); - console.log(newIndex); this.context.print(); } @@ -112,10 +114,6 @@ export class BaseMenu extends React.PureComponent { isExpanded () { return this.context.isOpenMenu(this.menuRef); } - - isInnermostExpanded () { - return this.context.isTopMenu(this.menuRef); - } } BaseMenu.propTypes = { diff --git a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx index 4e283170ac..8d651ef63c 100644 --- a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx @@ -2,7 +2,6 @@ import React from 'react'; import styles from './menu-bar.css'; import classNames from 'classnames'; import PropTypes from 'prop-types'; -import bindAll from 'lodash.bindall'; import editIcon from './icon--edit.svg'; import {FormattedMessage} from 'react-intl'; @@ -17,8 +16,6 @@ export class EditMenu extends BaseMenu { constructor (props) { super(props); - bindAll(this); - this.restoreRef = React.createRef(); this.turboRef = React.createRef(); @@ -62,6 +59,7 @@ export class EditMenu extends BaseMenu { onClick={this.props.onRestoreOption(handleRestore)} menuRef={this.restoreRef} onParentKeyPress={this.handleKeyPressOpenMenu} + isDisabled={!restorable} > {this.props.restoreOptionMessage(deletedItem)} diff --git a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx index 4b4c33d3a5..3f0b73c67f 100644 --- a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx @@ -2,7 +2,6 @@ import React from 'react'; import styles from './menu-bar.css'; import classNames from 'classnames'; import PropTypes from 'prop-types'; -import bindAll from 'lodash.bindall'; import fileIcon from './icon--file.svg'; import {FormattedMessage} from 'react-intl'; @@ -19,8 +18,6 @@ export class FileMenu extends BaseMenu { constructor (props) { super(props); - bindAll(this, ['handleOnClose', 'handleKeyPress', 'handleOnOpen']); - this.newProjectRef = React.createRef(); this.saveRef = React.createRef(); this.createRef = React.createRef(); diff --git a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx index e4c1a5f551..50d3de5d6e 100644 --- a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx @@ -43,10 +43,15 @@ class LanguageMenu extends BaseMenu { // If we are using hover rather than clicks for submenus, scroll the selected option into view if (!this.props.menuOpen && this.selectedRef) { this.selectedRef.scrollIntoView({block: 'center'}); - this.refocusRef(this.selectedRef); } } + handleOnOpen () { + super.handleOnOpen(); + this.refocusItemByIndex(Object.keys(locales).indexOf(this.props.currentLocale)); + } + + render () { const { currentLocale, diff --git a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx index 6cc0c8a458..69ee45fe74 100644 --- a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx @@ -2,7 +2,6 @@ import React from 'react'; import styles from './menu-bar.css'; import classNames from 'classnames'; import PropTypes from 'prop-types'; -import bindAll from 'lodash.bindall'; import {FormattedMessage} from 'react-intl'; import MenuBarMenu from './menu-bar-menu.jsx'; @@ -13,8 +12,6 @@ export class ModeMenu extends BaseMenu { constructor (props) { super(props); - bindAll(this); - this.normalRef = React.createRef(); this.caturdayRef = React.createRef(); diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx index 32b4227b55..74c6af2927 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx @@ -1,6 +1,5 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; -import bindAll from 'lodash.bindall'; import React from 'react'; import {FormattedMessage} from 'react-intl'; @@ -20,8 +19,6 @@ class SettingsMenu extends BaseMenu { constructor (props) { super(props); - bindAll(this, []); - this.languageRef = React.createRef(); this.themeRef = React.createRef(); // hardcoded logic because of only two options diff --git a/packages/scratch-gui/src/components/menu/menu.jsx b/packages/scratch-gui/src/components/menu/menu.jsx index 8e62587343..0a7e1136ce 100644 --- a/packages/scratch-gui/src/components/menu/menu.jsx +++ b/packages/scratch-gui/src/components/menu/menu.jsx @@ -63,6 +63,7 @@ const MenuItem = ({ className, expanded = false, isSelected = false, + isDisabled = false, onClick, menuRef, ariaLabel, @@ -81,6 +82,7 @@ const MenuItem = ({ ref={menuRef} aria-label={ariaLabel} aria-selected={isSelected ?? null} + aria-disabled={isDisabled ?? null} role={ariaRole} onKeyDown={onParentKeyPress} > @@ -96,6 +98,7 @@ MenuItem.propTypes = { className: PropTypes.string, expanded: PropTypes.bool, isSelected: PropTypes.bool, + isDisabled: PropTypes.bool, onClick: PropTypes.func, onParentKeyPress: PropTypes.func }; diff --git a/packages/scratch-gui/src/containers/gui.jsx b/packages/scratch-gui/src/containers/gui.jsx index 1890a6c6f1..d84fab6f7f 100644 --- a/packages/scratch-gui/src/containers/gui.jsx +++ b/packages/scratch-gui/src/containers/gui.jsx @@ -148,7 +148,7 @@ GUI.propTypes = { }; GUI.defaultProps = { - isTotallyNormal: true, + isTotallyNormal: false, onStorageInit: () => {}, onProjectLoaded: () => {}, onUpdateProjectId: () => {}, From 05fb1da610a068968c6c160125b4a86f1c4a544a Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 29 Dec 2025 14:33:51 +0200 Subject: [PATCH 09/66] chore: cleanup of isOpen logic for menus, since it is reimplemented --- .../src/components/menu-bar/language-menu.jsx | 3 +-- .../src/components/menu-bar/menu-bar.jsx | 12 ------------ packages/scratch-gui/src/reducers/menus.js | 14 +------------- 3 files changed, 2 insertions(+), 27 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx index 50d3de5d6e..b9c1503358 100644 --- a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx @@ -9,7 +9,7 @@ import locales from 'scratch-l10n'; import check from './check.svg'; import {MenuItem, Submenu} from '../menu/menu.jsx'; import languageIcon from '../language-selector/language-icon.svg'; -import {closeLanguageMenu, languageMenuOpen, openLanguageMenu} from '../../reducers/menus.js'; +import {closeLanguageMenu, openLanguageMenu} from '../../reducers/menus.js'; import {selectLocale} from '../../reducers/locales.js'; import styles from './settings-menu.css'; @@ -137,7 +137,6 @@ LanguageMenu.propTypes = { const mapStateToProps = state => ({ currentLocale: state.locales.locale, isRtl: state.locales.isRtl, - menuOpen: languageMenuOpen(state), messagesByLocale: state.locales.messagesByLocale }); diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx index 640bcdb4e4..bcc734c7eb 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx @@ -58,17 +58,13 @@ import { accountMenuOpen, openFileMenu, closeFileMenu, - fileMenuOpen, openEditMenu, closeEditMenu, - editMenuOpen, openLoginMenu, closeLoginMenu, loginMenuOpen, openModeMenu, closeModeMenu, - modeMenuOpen, - settingsMenuOpen, openSettingsMenu, closeSettingsMenu } from '../../reducers/menus'; @@ -774,9 +770,7 @@ MenuBar.propTypes = { className: PropTypes.string, confirmReadyToReplaceProject: PropTypes.func, currentLocale: PropTypes.string.isRequired, - editMenuOpen: PropTypes.bool, enableCommunity: PropTypes.bool, - fileMenuOpen: PropTypes.bool, intl: intlShape, isRtl: PropTypes.bool, isShared: PropTypes.bool, @@ -790,7 +784,6 @@ MenuBar.propTypes = { mode1990: PropTypes.bool, mode2020: PropTypes.bool, mode220022BC: PropTypes.bool, - modeMenuOpen: PropTypes.bool, modeNow: PropTypes.bool, onClickAbout: PropTypes.oneOfType([ PropTypes.func, // button mode: call this callback when the About button is clicked @@ -833,7 +826,6 @@ MenuBar.propTypes = { platform: PropTypes.oneOf(Object.keys(PLATFORM)), projectTitle: PropTypes.string, renderLogin: PropTypes.func, - settingsMenuOpen: PropTypes.bool, shouldSaveBeforeTransition: PropTypes.func, showComingSoon: PropTypes.bool, username: PropTypes.string, @@ -859,16 +851,12 @@ const mapStateToProps = (state, ownProps) => { aboutMenuOpen: aboutMenuOpen(state), accountMenuOpen: accountMenuOpen(state), currentLocale: state.locales.locale, - fileMenuOpen: fileMenuOpen(state), - editMenuOpen: editMenuOpen(state), isRtl: state.locales.isRtl, isUpdating: getIsUpdating(loadingState), isShowingProject: getIsShowingProject(loadingState), locale: state.locales.locale, loginMenuOpen: loginMenuOpen(state), - modeMenuOpen: modeMenuOpen(state), projectTitle: state.scratchGui.projectTitle, - settingsMenuOpen: settingsMenuOpen(state), username: ownProps.username ?? (user ? user.username : null), userIsEducator: permissions && permissions.educator, vm: state.scratchGui.vm, diff --git a/packages/scratch-gui/src/reducers/menus.js b/packages/scratch-gui/src/reducers/menus.js index 4fec54b8a6..f083f41c44 100644 --- a/packages/scratch-gui/src/reducers/menus.js +++ b/packages/scratch-gui/src/reducers/menus.js @@ -120,15 +120,12 @@ const accountMenuOpen = state => state.scratchGui.menus[MENU_ACCOUNT]; const openEditMenu = () => openMenu(MENU_EDIT); const closeEditMenu = () => closeMenu(MENU_EDIT); -const editMenuOpen = state => state.scratchGui.menus[MENU_EDIT]; const openFileMenu = () => openMenu(MENU_FILE); const closeFileMenu = () => closeMenu(MENU_FILE); -const fileMenuOpen = state => state.scratchGui.menus[MENU_FILE]; const openLanguageMenu = () => openMenu(MENU_LANGUAGE); const closeLanguageMenu = () => closeMenu(MENU_LANGUAGE); -const languageMenuOpen = state => state.scratchGui.menus[MENU_LANGUAGE]; const openLoginMenu = () => openMenu(MENU_LOGIN); const closeLoginMenu = () => closeMenu(MENU_LOGIN); @@ -136,15 +133,12 @@ const loginMenuOpen = state => state.scratchGui.menus[MENU_LOGIN]; const openModeMenu = () => openMenu(MENU_MODE); const closeModeMenu = () => closeMenu(MENU_MODE); -const modeMenuOpen = state => state.scratchGui.menus[MENU_MODE]; const openSettingsMenu = () => openMenu(MENU_SETTINGS); const closeSettingsMenu = () => closeMenu(MENU_SETTINGS); -const settingsMenuOpen = state => state.scratchGui.menus[MENU_SETTINGS]; const openThemeMenu = () => openMenu(MENU_THEME); const closeThemeMenu = () => closeMenu(MENU_THEME); -const themeMenuOpen = state => state.scratchGui.menus[MENU_THEME]; export { reducer as default, @@ -157,23 +151,17 @@ export { accountMenuOpen, openEditMenu, closeEditMenu, - editMenuOpen, openFileMenu, closeFileMenu, - fileMenuOpen, openLanguageMenu, closeLanguageMenu, - languageMenuOpen, openLoginMenu, closeLoginMenu, loginMenuOpen, openModeMenu, closeModeMenu, - modeMenuOpen, openSettingsMenu, closeSettingsMenu, - settingsMenuOpen, openThemeMenu, - closeThemeMenu, - themeMenuOpen + closeThemeMenu }; From 333f1f837b73e386fc370bf1bd20da2f85836b61 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 29 Dec 2025 15:17:52 +0200 Subject: [PATCH 10/66] chore: adjusted key press logic to match wanted functionality --- packages/scratch-gui/src/components/menu-bar/base-menu.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/scratch-gui/src/components/menu-bar/base-menu.jsx b/packages/scratch-gui/src/components/menu-bar/base-menu.jsx index b88ae7e3d5..bb1fb13ec1 100644 --- a/packages/scratch-gui/src/components/menu-bar/base-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/base-menu.jsx @@ -61,7 +61,7 @@ export class BaseMenu extends React.PureComponent { if (this.context.isTopMenu(this.menuRef)) { this.handleKeyPressOpenMenu(e); - } else if (!this.isExpanded() && (e.key === ' ' || e.key === 'ArrowRight')) { + } else if (!this.isExpanded() && (e.key === ' ' || (e.key === 'ArrowRight' && this.props.depth !== 1))) { e.preventDefault(); this.handleOnOpen(); } From 2ce4eac65bce18023bdd0452a1f29d56d2c829dd Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Tue, 30 Dec 2025 12:57:54 +0200 Subject: [PATCH 11/66] chore: post-merge accessibility readjustments --- .../src/components/menu-bar/base-menu.jsx | 2 +- .../components/menu-bar/preference-menu.jsx | 123 ++++---- .../src/components/menu-bar/settings-menu.jsx | 32 +-- .../src/components/menu-bar/theme-menu.jsx | 271 ------------------ .../scratch-gui/src/components/menu/menu.jsx | 4 +- 5 files changed, 91 insertions(+), 341 deletions(-) delete mode 100644 packages/scratch-gui/src/components/menu-bar/theme-menu.jsx diff --git a/packages/scratch-gui/src/components/menu-bar/base-menu.jsx b/packages/scratch-gui/src/components/menu-bar/base-menu.jsx index 581673934d..7dc54cc8e7 100644 --- a/packages/scratch-gui/src/components/menu-bar/base-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/base-menu.jsx @@ -8,7 +8,7 @@ _______________________________________________ render define this.itemRefs add onKeyDown={this.handleKeyPress} -and onParentKeyPress={this.handleKeyPressSubmenu} for MenuItem elements +and onParentKeyPress={this.handleKeyPressOpenMenu} for MenuItem elements and replace isOpenMenu-like props with this.isExpanded() checks diff --git a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx index b1a863e44c..b5a00b7eb8 100644 --- a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx @@ -1,11 +1,12 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; -import React, {useMemo} from 'react'; +import React from 'react'; import {FormattedMessage} from 'react-intl'; import {connect} from 'react-redux'; import check from './check.svg'; import {MenuItem, Submenu} from '../menu/menu.jsx'; +import {BaseMenu} from './base-menu.jsx'; import styles from './settings-menu.css'; @@ -21,7 +22,12 @@ const PreferenceItem = props => { const item = props.item; return ( - +
    { - const itemKeys = useMemo(() => Object.keys(itemsMap), [itemsMap]); - const selectedItem = useMemo(() => itemsMap[selectedItemKey], [itemsMap, selectedItemKey]); - return ( - -
    - - - - - -
    - - {itemKeys.map(itemKey => ( - onChange(itemKey)} - item={itemsMap[itemKey]} - />) - )} - -
    - ); +class PreferenceMenu extends BaseMenu { + constructor (props) { + super(props); + + this.itemRefs = Object.keys(this.props.itemsMap).map(() => React.createRef()); + } + + render () { + const { + itemsMap, + onChange, + defaultMenuIconSrc, + submenuLabel, + selectedItemKey, + isRtl, + menuRef + } = this.props; + + const itemKeys = Object.keys(itemsMap); + const selectedItem = itemsMap[selectedItemKey]; + + return ( + +
    + + + + + +
    + + {itemKeys.map((itemKey, index) => ( + onChange(itemKey)} + item={itemsMap[itemKey]} + menuRef={this.itemRefs[index]} + />) + )} + +
    + ); + } }; PreferenceMenu.propTypes = { @@ -95,14 +120,12 @@ PreferenceMenu.propTypes = { icon: PropTypes.string, label: intlMessageShape.isRequired })).isRequired, - open: PropTypes.bool, onChange: PropTypes.func, - onRequestCloseSettings: PropTypes.func, - onRequestOpen: PropTypes.func, defaultMenuIconSrc: PropTypes.string, submenuLabel: intlMessageShape.isRequired, selectedItemKey: PropTypes.string, - isRtl: PropTypes.bool + isRtl: PropTypes.bool, + menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}) }; const mapStateToProps = state => ({ diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx index 6f5589b0de..1936a88262 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; -import React, {useMemo} from 'react'; +import React from 'react'; import {FormattedMessage} from 'react-intl'; import {connect} from 'react-redux'; @@ -27,15 +27,18 @@ import {colorModeMenuOpen, openColorModeMenu, openThemeMenu} from '../../reducer const enabledColorModes = [DEFAULT_MODE, HIGH_CONTRAST_MODE]; - class SettingsMenu extends BaseMenu { constructor (props) { super(props); this.languageRef = React.createRef(); - this.preferenceRef = React.createRef(); - // hardcoded logic because of only two options - this.itemRefs = [this.languageRef, this.preferenceRef]; + this.themeRef = React.createRef(); + this.colorRef = React.createRef(); + this.itemRefs = [ + ...(this.props.canChangeLanguage ? [this.languageRef] : []), + ...(this.props.canChangeTheme && this.props.availableThemesLength > 1 ? [this.themeRef] : []), + ...(this.props.canChangeColorMode ? [this.colorRef] : []) + ]; } render () { @@ -45,8 +48,6 @@ class SettingsMenu extends BaseMenu { canChangeTheme, hasActiveMembership, isRtl, - isColorModeMenuOpen, - isThemeMenuOpen, activeColorMode, onChangeColorMode, onRequestOpenColorMode, @@ -55,7 +56,6 @@ class SettingsMenu extends BaseMenu { onChangeTheme } = this.props; - const enabledColorModesMap = Object.keys(colorModeMap).reduce((acc, colorMode) => { if (enabledColorModes.includes(colorMode)) { acc[colorMode] = colorModeMap[colorMode]; @@ -107,9 +107,8 @@ class SettingsMenu extends BaseMenu { // TODO: Consider always showing the theme menu, even if there is a single available theme availableThemesLength > 1 && } {canChangeColorMode && } @@ -172,12 +170,12 @@ const mapDispatchToProps = (dispatch, ownProps) => ({ }, onChangeColorMode: colorMode => { dispatch(setColorMode(colorMode)); - ownProps.onRequestClose(); + ownProps.onClose(); persistColorMode(colorMode); }, onChangeTheme: theme => { dispatch(setTheme(theme)); - ownProps.onRequestClose(); + ownProps.onClose(); persistTheme(theme); } }); diff --git a/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx b/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx deleted file mode 100644 index b1acb27a39..0000000000 --- a/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx +++ /dev/null @@ -1,271 +0,0 @@ -// import classNames from 'classnames'; -// import PropTypes from 'prop-types'; -// import bindAll from 'lodash.bindall'; -// import React from 'react'; -// import {FormattedMessage} from 'react-intl'; -// import {connect} from 'react-redux'; - -// import check from './check.svg'; -// import {MenuItem, Submenu} from '../menu/menu.jsx'; -// import {DEFAULT_THEME, HIGH_CONTRAST_THEME, themeMap} from '../../lib/themes'; -// import {persistTheme} from '../../lib/themes/themePersistance'; -// import {openThemeMenu, closeThemeMenu} from '../../reducers/menus.js'; -// import {setTheme} from '../../reducers/theme.js'; - -// import styles from './settings-menu.css'; - -// import dropdownCaret from './dropdown-caret.svg'; -// import {BaseMenu} from './base-menu'; - -// const ThemeMenuItem = props => { -// const themeInfo = themeMap[props.theme]; - -// return ( -// -//
    -// -// -// -//
    -//
    ); -// }; - -// ThemeMenuItem.propTypes = { -// isSelected: PropTypes.bool, -// onClick: PropTypes.func, -// theme: PropTypes.string, -// menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), -// onParentKeyPress: PropTypes.func -// }; - -// class ThemeMenu extends BaseMenu { -// constructor (props) { -// super(props); -// bindAll(this, [ -// 'setRef' -// ]); - -// this.enabledThemes = [DEFAULT_THEME, HIGH_CONTRAST_THEME]; -// this.itemRefs = this.enabledThemes.map(() => React.createRef()); -// } - -// setRef (component) { -// this.selectedRef = component; -// } - -// render () { -// const { -// menuRef, -// isRtl, -// onChangeTheme, -// theme -// } = this.props; - -// const themeInfo = themeMap[theme]; - -// return ( -// -//
    -// -// -// -// -// -//
    -// -// {this.enabledThemes.map((enabledTheme, index) => ( -// onChangeTheme(enabledTheme)} -// theme={enabledTheme} -// menuRef={this.itemRefs[index]} -// onParentKeyPress={this.handleKeyPressOpenMenu} -// />) -// )} -// -//
    -// ); -// } -// } - -// ThemeMenu.propTypes = { -// menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), -// isRtl: PropTypes.bool, -// onChangeTheme: PropTypes.func, -// onOpen: PropTypes.func, -// onClose: PropTypes.func, -// theme: PropTypes.string -// }; - -// const mapStateToProps = state => ({ -// isRtl: state.locales.isRtl, -// theme: state.scratchGui.theme.theme -// }); - -// const mapDispatchToProps = dispatch => ({ -// onChangeTheme: theme => { -// dispatch(setTheme(theme)); -// persistTheme(theme); -// }, -// onOpen: () => dispatch(openThemeMenu()), -// onClose: () => dispatch(closeThemeMenu()) -// }); - -// export default connect( -// mapStateToProps, -// mapDispatchToProps -// )(ThemeMenu); - -// /* -// import classNames from 'classnames'; -// import PropTypes from 'prop-types'; -// import React from 'react'; -// import {FormattedMessage} from 'react-intl'; -// import {connect} from 'react-redux'; - -// import check from './check.svg'; -// import {MenuItem, Submenu} from '../menu/menu.jsx'; -// import {DEFAULT_THEME, HIGH_CONTRAST_THEME, themeMap} from '../../lib/themes'; -// import {persistTheme} from '../../lib/themes/themePersistance'; -// import {openThemeMenu, themeMenuOpen} from '../../reducers/menus.js'; -// import {setTheme} from '../../reducers/theme.js'; - -// import styles from './settings-menu.css'; - -// import dropdownCaret from './dropdown-caret.svg'; - -// const ThemeMenuItem = props => { -// const themeInfo = themeMap[props.theme]; - -// return ( -// -//
    -// -// -// -//
    -//
    ); -// }; - -// ThemeMenuItem.propTypes = { -// isSelected: PropTypes.bool, -// onClick: PropTypes.func, -// theme: PropTypes.string -// }; - -// const ThemeMenu = ({ -// isRtl, -// menuOpen, -// onChangeTheme, -// onRequestOpen, -// theme -// }) => { -// const enabledThemes = [DEFAULT_THEME, HIGH_CONTRAST_THEME]; -// const themeInfo = themeMap[theme]; - -// return ( -// -//
    -// -// -// -// -// -//
    -// -// {enabledThemes.map(enabledTheme => ( -// onChangeTheme(enabledTheme)} -// theme={enabledTheme} -// />) -// )} -// -//
    -// ); -// }; - -// ThemeMenu.propTypes = { -// isRtl: PropTypes.bool, -// menuOpen: PropTypes.bool, -// onChangeTheme: PropTypes.func, -// // eslint-disable-next-line react/no-unused-prop-types -// onRequestCloseSettings: PropTypes.func, -// onRequestOpen: PropTypes.func, -// theme: PropTypes.string -// }; - -// const mapStateToProps = state => ({ -// isRtl: state.locales.isRtl, -// menuOpen: themeMenuOpen(state), -// theme: state.scratchGui.theme.theme -// }); - -// const mapDispatchToProps = (dispatch, ownProps) => ({ -// onChangeTheme: theme => { -// dispatch(setTheme(theme)); -// ownProps.onRequestCloseSettings(); -// persistTheme(theme); -// }, -// onRequestOpen: () => dispatch(openThemeMenu()) -// }); - -// export default connect( -// mapStateToProps, -// mapDispatchToProps -// )(ThemeMenu); -// */ diff --git a/packages/scratch-gui/src/components/menu/menu.jsx b/packages/scratch-gui/src/components/menu/menu.jsx index 0a7e1136ce..de988c6285 100644 --- a/packages/scratch-gui/src/components/menu/menu.jsx +++ b/packages/scratch-gui/src/components/menu/menu.jsx @@ -81,8 +81,8 @@ const MenuItem = ({ tabIndex={-1} ref={menuRef} aria-label={ariaLabel} - aria-selected={isSelected ?? null} - aria-disabled={isDisabled ?? null} + aria-selected={isSelected} + aria-disabled={isDisabled} role={ariaRole} onKeyDown={onParentKeyPress} > From fdac2f409198b6d93a04c481953ed41e807c1788 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Tue, 30 Dec 2025 14:35:36 +0200 Subject: [PATCH 12/66] chore: minor code changes --- packages/scratch-gui/src/components/menu-bar/base-menu.jsx | 4 +++- .../scratch-gui/src/components/menu-bar/settings-menu.jsx | 6 ++---- packages/scratch-gui/src/reducers/menus.js | 2 -- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/base-menu.jsx b/packages/scratch-gui/src/components/menu-bar/base-menu.jsx index 7dc54cc8e7..f33dff1fa1 100644 --- a/packages/scratch-gui/src/components/menu-bar/base-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/base-menu.jsx @@ -19,7 +19,7 @@ onClose, menuRef, depth */ -export class BaseMenu extends React.PureComponent { +class BaseMenu extends React.PureComponent { constructor (props) { super(props); bindAll(this, [ @@ -127,3 +127,5 @@ BaseMenu.defaultProps = { onClose: () => {}, clearOnItemSelect: false }; + +export default BaseMenu; diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx index 1936a88262..beebebf32e 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx @@ -23,7 +23,7 @@ import settingsIcon from './icon--settings.svg'; import {BaseMenu} from './base-menu.jsx'; import themeIcon from '../../lib/assets/icon--theme.svg'; -import {colorModeMenuOpen, openColorModeMenu, openThemeMenu} from '../../reducers/menus.js'; +import {openColorModeMenu, openThemeMenu} from '../../reducers/menus.js'; const enabledColorModes = [DEFAULT_MODE, HIGH_CONTRAST_MODE]; @@ -149,7 +149,6 @@ SettingsMenu.propTypes = { activeColorMode: PropTypes.string, onChangeColorMode: PropTypes.func, onRequestOpenColorMode: PropTypes.func, - isColorModeMenuOpen: PropTypes.bool, activeTheme: PropTypes.string, onChangeTheme: PropTypes.func, onRequestOpenTheme: PropTypes.func @@ -157,8 +156,7 @@ SettingsMenu.propTypes = { const mapStateToProps = state => ({ activeColorMode: state.scratchGui.settings.colorMode, - activeTheme: state.scratchGui.settings.theme, - isColorModeMenuOpen: colorModeMenuOpen(state) + activeTheme: state.scratchGui.settings.theme }); const mapDispatchToProps = (dispatch, ownProps) => ({ diff --git a/packages/scratch-gui/src/reducers/menus.js b/packages/scratch-gui/src/reducers/menus.js index 094cef8d1b..42387b8cd0 100644 --- a/packages/scratch-gui/src/reducers/menus.js +++ b/packages/scratch-gui/src/reducers/menus.js @@ -142,7 +142,6 @@ const closeSettingsMenu = () => closeMenu(MENU_SETTINGS); const openColorModeMenu = () => openMenu(MENU_COLOR_MODE); const closeColorModeMenu = () => closeMenu(MENU_COLOR_MODE); -const colorModeMenuOpen = state => state.scratchGui.menus[MENU_COLOR_MODE]; const openThemeMenu = () => openMenu(MENU_THEME); const closeThemeMenu = () => closeMenu(MENU_THEME); @@ -171,7 +170,6 @@ export { closeSettingsMenu, openColorModeMenu, closeColorModeMenu, - colorModeMenuOpen, openThemeMenu, closeThemeMenu }; From 77f2ac6cedddd9f43d5861050bf70a3ff0207e6d Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Tue, 30 Dec 2025 19:35:51 +0200 Subject: [PATCH 13/66] chore: translated labels and some more adjustments --- .../src/components/menu-bar/edit-menu.jsx | 14 ++++- .../src/components/menu-bar/file-menu.jsx | 55 +++++++++++++------ .../src/components/menu-bar/language-menu.jsx | 16 ++++-- .../src/components/menu-bar/menu-bar.jsx | 10 +++- .../src/components/menu-bar/mode-menu.jsx | 32 ++++++++--- .../components/menu-bar/preference-menu.jsx | 7 ++- .../src/components/menu-bar/settings-menu.jsx | 30 +++++++++- 7 files changed, 126 insertions(+), 38 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx index 8d651ef63c..e3f33b5dcd 100644 --- a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx @@ -4,13 +4,20 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import editIcon from './icon--edit.svg'; -import {FormattedMessage} from 'react-intl'; +import {FormattedMessage, defineMessage} from 'react-intl'; import MenuBarMenu from './menu-bar-menu.jsx'; import {MenuItem, MenuSection} from '../menu/menu.jsx'; -import {BaseMenu} from './base-menu'; +import BaseMenu from './base-menu'; import dropdownCaret from './dropdown-caret.svg'; import DeletionRestorer from '../../containers/deletion-restorer.jsx'; import TurboMode from '../../containers/turbo-mode.jsx'; +import intlShape from '../../lib/intlShape.js'; + +const editMenu = defineMessage({ + id: 'editMenu.aria.editMenu', + defaultMessage: 'Edit menu', + description: 'ARIA label for edit menu' +}); export class EditMenu extends BaseMenu { constructor (props) { @@ -33,7 +40,7 @@ export class EditMenu extends BaseMenu { })} onClick={this.handleOnOpen} role="button" - aria-label="Edit Menu" + aria-label={this.props.intl.formatMessage(editMenu)} aria-expanded={this.isExpanded()} tabIndex={0} onKeyDown={this.handleKeyPress} @@ -94,6 +101,7 @@ export class EditMenu extends BaseMenu { } EditMenu.propTypes = { + intl: intlShape.isRequired, menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), isRtl: PropTypes.bool, restoreOptionMessage: PropTypes.func, diff --git a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx index 3f0b73c67f..559c920f03 100644 --- a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx @@ -4,16 +4,22 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import fileIcon from './icon--file.svg'; -import {FormattedMessage} from 'react-intl'; +import {FormattedMessage, defineMessage} from 'react-intl'; import MenuBarMenu from './menu-bar-menu.jsx'; import {MenuItem, MenuSection} from '../menu/menu.jsx'; -import {BaseMenu} from './base-menu'; +import BaseMenu from './base-menu'; import SB3Downloader from '../../containers/sb3-downloader.jsx'; import dropdownCaret from './dropdown-caret.svg'; import sharedMessages from '../../lib/shared-messages'; import intlShape from '../../lib/intlShape.js'; +const fileMenu = defineMessage({ + id: 'fileMenu.aria.fileMenu', + defaultMessage: 'File menu', + description: 'ARIA label for file menu' +}); + export class FileMenu extends BaseMenu { constructor (props) { super(props); @@ -36,6 +42,21 @@ export class FileMenu extends BaseMenu { } render () { + const { + intl, + isRtl, + menuRef, + canSave, + canCreateCopy, + canRemix, + onClickNew, + onClickSave, + onClickSaveAsCopy, + onClickRemix, + onStartSelectingFileUpload, + getSaveToComputerHandler + } = this.props; + const saveNowMessage = ( @@ -89,42 +110,42 @@ export class FileMenu extends BaseMenu { {newProjectMessage} - {(this.props.canSave || this.props.canCreateCopy || this.props.canRemix) && ( + {(canSave || canCreateCopy || canRemix) && ( - {this.props.canSave && ( + {canSave && ( {saveNowMessage} )} - {this.props.canCreateCopy && ( + {canCreateCopy && ( {createCopyMessage} )} - {this.props.canRemix && ( + {canRemix && ( @@ -135,16 +156,16 @@ export class FileMenu extends BaseMenu { )} - {this.props.intl.formatMessage(sharedMessages.loadFromComputerTitle)} + {intl.formatMessage(sharedMessages.loadFromComputerTitle)} {(className, downloadProjectCallback) => ( diff --git a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx index b9c1503358..7c47efd370 100644 --- a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx @@ -2,7 +2,7 @@ import classNames from 'classnames'; import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; import React from 'react'; -import {FormattedMessage} from 'react-intl'; +import {FormattedMessage, defineMessage} from 'react-intl'; import {connect} from 'react-redux'; import locales from 'scratch-l10n'; @@ -13,9 +13,16 @@ import {closeLanguageMenu, openLanguageMenu} from '../../reducers/menus.js'; import {selectLocale} from '../../reducers/locales.js'; import styles from './settings-menu.css'; +import intlShape from '../../lib/intlShape.js'; import dropdownCaret from './dropdown-caret.svg'; -import {BaseMenu} from './base-menu'; +import BaseMenu from './base-menu'; + +const languageMenu = defineMessage({ + id: 'languageMenu.aria.languageMenu', + defaultMessage: 'Language menu', + description: 'ARIA label for language menu' +}); class LanguageMenu extends BaseMenu { constructor (props) { @@ -51,9 +58,9 @@ class LanguageMenu extends BaseMenu { this.refocusItemByIndex(Object.keys(locales).indexOf(this.props.currentLocale)); } - render () { const { + intl, currentLocale, menuRef, isRtl, @@ -67,7 +74,7 @@ class LanguageMenu extends BaseMenu { onClick={this.handleOnOpen} onMouseOver={this.handleMouseOver} ref={menuRef} - aria-label="Language Menu" + aria-label={intl.formatMessage(languageMenu)} aria-expanded={this.isExpanded()} role="button" tabIndex={-1} @@ -125,6 +132,7 @@ class LanguageMenu extends BaseMenu { } LanguageMenu.propTypes = { + intl: intlShape, currentLocale: PropTypes.string, menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), isRtl: PropTypes.bool, diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx index c57bcbeca9..4683d1626f 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx @@ -103,6 +103,11 @@ const ariaMessages = defineMessages({ id: 'gui.menuBar.debug', defaultMessage: 'Debug', description: 'accessibility text for the debug button' + }, + goHome: { + id: 'gui.menuBar.goHome', + defaultMessage: 'Go home', + description: 'accessibility text for the go home button' } }); @@ -428,7 +433,7 @@ class MenuBar extends React.Component {
    Scratch)}
    diff --git a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx index 69ee45fe74..648da4c124 100644 --- a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx @@ -3,10 +3,17 @@ import styles from './menu-bar.css'; import classNames from 'classnames'; import PropTypes from 'prop-types'; -import {FormattedMessage} from 'react-intl'; +import {FormattedMessage, defineMessage} from 'react-intl'; import MenuBarMenu from './menu-bar-menu.jsx'; import {MenuItem, MenuSection} from '../menu/menu.jsx'; -import {BaseMenu} from './base-menu'; +import BaseMenu from './base-menu'; + +import intlShape from '../../lib/intlShape.js'; +const modeMenu = defineMessage({ + id: 'modeMenu.aria.modeMenu', + defaultMessage: 'Mode menu', + description: 'ARIA label for mode menu' +}); export class ModeMenu extends BaseMenu { constructor (props) { @@ -22,6 +29,14 @@ export class ModeMenu extends BaseMenu { } render () { + const { + intl, + isRtl, + mode2020, + modeNow, + onSetMode + } = this.props; + return (
    - + {'✓'} {' '} @@ -64,11 +79,11 @@ export class ModeMenu extends BaseMenu { /> - + {'✓'} {' '} @@ -86,6 +101,7 @@ export class ModeMenu extends BaseMenu { } ModeMenu.propTypes = { + intl: intlShape, menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), onSetMode: PropTypes.func, modeNow: PropTypes.bool, diff --git a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx index b5a00b7eb8..2fbbd2560f 100644 --- a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx @@ -6,7 +6,7 @@ import {connect} from 'react-redux'; import check from './check.svg'; import {MenuItem, Submenu} from '../menu/menu.jsx'; -import {BaseMenu} from './base-menu.jsx'; +import BaseMenu from './base-menu.jsx'; import styles from './settings-menu.css'; @@ -68,7 +68,8 @@ class PreferenceMenu extends BaseMenu { submenuLabel, selectedItemKey, isRtl, - menuRef + menuRef, + ariaLabel } = this.props; const itemKeys = Object.keys(itemsMap); @@ -81,6 +82,7 @@ class PreferenceMenu extends BaseMenu { onClick={this.handleOnOpen} ref={menuRef} aria-expanded={this.isExpanded()} + aria-label={ariaLabel} role="button" tabIndex={-1} onKeyDown={this.handleKeyPress} @@ -116,6 +118,7 @@ class PreferenceMenu extends BaseMenu { }; PreferenceMenu.propTypes = { + ariaLabel: PropTypes.string, itemsMap: PropTypes.objectOf(PropTypes.shape({ icon: PropTypes.string, label: intlMessageShape.isRequired diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx index beebebf32e..dfa9becc1d 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx @@ -1,7 +1,7 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; -import {FormattedMessage} from 'react-intl'; +import {FormattedMessage, defineMessage, defineMessages} from 'react-intl'; import {connect} from 'react-redux'; import LanguageMenu from './language-menu.jsx'; @@ -20,10 +20,29 @@ import styles from './settings-menu.css'; import dropdownCaret from './dropdown-caret.svg'; import settingsIcon from './icon--settings.svg'; -import {BaseMenu} from './base-menu.jsx'; +import BaseMenu from './base-menu.jsx'; import themeIcon from '../../lib/assets/icon--theme.svg'; import {openColorModeMenu, openThemeMenu} from '../../reducers/menus.js'; +import intlShape from '../../lib/intlShape.js'; + +const ariaMessages = defineMessages({ + settingsMenu: { + id: 'settingsMenu.aria.settingsMenu', + defaultMessage: 'Settings menu', + description: 'ARIA label for settings menu' + }, + themeMenu: { + id: 'settingsMenu.aria.themeMenu', + defaultMessage: 'Theme menu', + description: 'ARIA label for theme menu' + }, + colorMenu: { + id: 'settingsMenu.aria.colorMenu', + defaultMessage: 'Color menu', + description: 'ARIA label for color menu' + } +}); const enabledColorModes = [DEFAULT_MODE, HIGH_CONTRAST_MODE]; @@ -47,6 +66,7 @@ class SettingsMenu extends BaseMenu { canChangeColorMode, canChangeTheme, hasActiveMembership, + intl, isRtl, activeColorMode, onChangeColorMode, @@ -78,7 +98,7 @@ class SettingsMenu extends BaseMenu { role="button" aria-expanded={this.isExpanded()} tabIndex={0} - aria-label="Settings" + aria-label={intl.formatMessage(ariaMessages.settingsMenu)} onClick={this.handleOnOpen} onKeyDown={this.handleKeyPress} ref={this.menuRef} @@ -100,6 +120,7 @@ class SettingsMenu extends BaseMenu { > {canChangeLanguage && } @@ -107,6 +128,7 @@ class SettingsMenu extends BaseMenu { // TODO: Consider always showing the theme menu, even if there is a single available theme availableThemesLength > 1 && } {canChangeColorMode && Date: Mon, 5 Jan 2026 10:18:59 +0200 Subject: [PATCH 14/66] chore: undeleted a line --- packages/scratch-gui/src/components/menu-bar/settings-menu.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx index dfa9becc1d..7a24886ac1 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx @@ -169,6 +169,7 @@ SettingsMenu.propTypes = { canChangeLanguage: PropTypes.bool, canChangeColorMode: PropTypes.bool, canChangeTheme: PropTypes.bool, + hasActiveMembership: PropTypes.bool, isRtl: PropTypes.bool, activeColorMode: PropTypes.string, onChangeColorMode: PropTypes.func, From 83bdad7f5f9442efe10ce94812ba29761a65e67e Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Tue, 6 Jan 2026 17:56:01 +0200 Subject: [PATCH 15/66] chore: moved file --- packages/scratch-gui/src/components/gui/gui.jsx | 2 +- packages/scratch-gui/src/components/menu-bar/base-menu.jsx | 2 +- .../{components/context-menu => contexts}/menu-ref-context.jsx | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/scratch-gui/src/{components/context-menu => contexts}/menu-ref-context.jsx (100%) diff --git a/packages/scratch-gui/src/components/gui/gui.jsx b/packages/scratch-gui/src/components/gui/gui.jsx index 69c1b31714..9828331083 100644 --- a/packages/scratch-gui/src/components/gui/gui.jsx +++ b/packages/scratch-gui/src/components/gui/gui.jsx @@ -46,7 +46,7 @@ import DebugModal from '../debug-modal/debug-modal.jsx'; import {setPlatform} from '../../reducers/platform.js'; import {setTheme} from '../../reducers/settings.js'; import {PLATFORM} from '../../lib/platform.js'; -import {MenuRefProvider} from '../context-menu/menu-ref-context.jsx'; +import {MenuRefProvider} from '../../contexts/menu-ref-context.jsx'; // Cache this value to only retrieve it once the first time. // Assume that it doesn't change for a session. diff --git a/packages/scratch-gui/src/components/menu-bar/base-menu.jsx b/packages/scratch-gui/src/components/menu-bar/base-menu.jsx index f33dff1fa1..ed8c0437b5 100644 --- a/packages/scratch-gui/src/components/menu-bar/base-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/base-menu.jsx @@ -1,4 +1,4 @@ -import {MenuRefContext} from '../context-menu/menu-ref-context'; +import {MenuRefContext} from '../../contexts/menu-ref-context'; import React from 'react'; import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; diff --git a/packages/scratch-gui/src/components/context-menu/menu-ref-context.jsx b/packages/scratch-gui/src/contexts/menu-ref-context.jsx similarity index 100% rename from packages/scratch-gui/src/components/context-menu/menu-ref-context.jsx rename to packages/scratch-gui/src/contexts/menu-ref-context.jsx From 5640a765d9a44d0d4219964a6bb7ddc4a70483e2 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Wed, 7 Jan 2026 10:25:31 +0200 Subject: [PATCH 16/66] chore: beginning to remove now obsolete old logic related to opening menus --- .../scratch-gui/src/components/menu-bar/base-menu.jsx | 8 ++++---- packages/scratch-gui/src/components/menu-bar/menu-bar.jsx | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/base-menu.jsx b/packages/scratch-gui/src/components/menu-bar/base-menu.jsx index ed8c0437b5..4c10913d20 100644 --- a/packages/scratch-gui/src/components/menu-bar/base-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/base-menu.jsx @@ -88,7 +88,7 @@ class BaseMenu extends React.PureComponent { handleOnOpen () { if (this.context.isOpenMenu(this.menuRef)) return; - this.props.onOpen(); + // this.props.onOpen(); this.refocusItemByIndex(0); this.context.push(this.menuRef, this.props.depth); @@ -107,7 +107,7 @@ class BaseMenu extends React.PureComponent { this.refocusRef(this.menuRef); }); - this.props.onClose(); + // this.props.onClose(); } isExpanded () { @@ -118,8 +118,8 @@ class BaseMenu extends React.PureComponent { BaseMenu.propTypes = { menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), depth: PropTypes.number, - onOpen: PropTypes.func, - onClose: PropTypes.func, + // onOpen: PropTypes.func, + // onClose: PropTypes.func, clearOnItemSelect: PropTypes.bool }; diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx index 4683d1626f..fe8fcb4f7e 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx @@ -461,8 +461,8 @@ class MenuBar extends React.Component { {(this.props.canManageFiles) && ( Date: Thu, 8 Jan 2026 12:55:26 +0200 Subject: [PATCH 17/66] chore: refactored menu navigation logic via a hook --- .../src/components/menu-bar/base-menu.jsx | 131 ------- .../src/components/menu-bar/edit-menu.jsx | 169 +++++---- .../src/components/menu-bar/file-menu.jsx | 336 ++++++++++-------- .../src/components/menu-bar/language-menu.jsx | 215 ++++++----- .../components/menu-bar/preference-menu.jsx | 123 ++++--- .../src/components/menu-bar/settings-menu.jsx | 255 ++++++------- .../src/contexts/menu-ref-context.jsx | 3 +- .../src/hooks/use-menu-navigation.jsx | 122 +++++++ 8 files changed, 698 insertions(+), 656 deletions(-) delete mode 100644 packages/scratch-gui/src/components/menu-bar/base-menu.jsx create mode 100644 packages/scratch-gui/src/hooks/use-menu-navigation.jsx diff --git a/packages/scratch-gui/src/components/menu-bar/base-menu.jsx b/packages/scratch-gui/src/components/menu-bar/base-menu.jsx deleted file mode 100644 index 4c10913d20..0000000000 --- a/packages/scratch-gui/src/components/menu-bar/base-menu.jsx +++ /dev/null @@ -1,131 +0,0 @@ -import {MenuRefContext} from '../../contexts/menu-ref-context'; -import React from 'react'; -import bindAll from 'lodash.bindall'; -import PropTypes from 'prop-types'; - -/* Subclasses must implement (some optionally): -_______________________________________________ -render -define this.itemRefs -add onKeyDown={this.handleKeyPress} -and onParentKeyPress={this.handleKeyPressOpenMenu} for MenuItem elements - -and replace isOpenMenu-like props with this.isExpanded() checks - -They should also receive: -______________________ -onOpen, -onClose, -menuRef, -depth -*/ -class BaseMenu extends React.PureComponent { - constructor (props) { - super(props); - bindAll(this, [ - 'handleKeyPress', - 'handleKeyPressOpenMenu', - 'handleMove', - 'handleOnOpen', - 'handleOnClose', - 'refocusRef', - 'refocusItemByIndex', - 'isExpanded' - ]); - - this.state = {focusedIndex: -1}; - this.menuRef = props.menuRef; - } - - static contextType = MenuRefContext; - - refocusRef (ref) { - if (ref && ref.current) { - ref.current.focus(); - } - } - - refocusItemByIndex (index) { - this.setState({focusedIndex: index}, () => { - this.refocusRef(this.itemRefs[index]); - }); - } - - handleKeyPress (e) { - if (this.props.depth === 1) { - if (e.key === 'Tab') { - this.handleOnClose(); - this.context.clear(); - } - } - - if (this.context.isTopMenu(this.menuRef)) { - this.handleKeyPressOpenMenu(e); - } else if (!this.isExpanded() && (e.key === ' ' || (e.key === 'ArrowRight' && this.props.depth !== 1))) { - e.preventDefault(); - this.handleOnOpen(); - } - } - - handleKeyPressOpenMenu (e) { - if (e.key === 'ArrowDown') { - e.preventDefault(); - this.handleMove(1); - } - if (e.key === 'ArrowUp') { - e.preventDefault(); - this.handleMove(-1); - } - if (e.key === 'Enter' && this.props.clearOnItemSelect) { - this.context.clear(); - } - if (e.key === 'ArrowLeft' || e.key === 'Escape') { - e.preventDefault(); - this.handleOnClose(); - } - } - - handleOnOpen () { - if (this.context.isOpenMenu(this.menuRef)) return; - - // this.props.onOpen(); - this.refocusItemByIndex(0); - - this.context.push(this.menuRef, this.props.depth); - } - - handleMove (direction) { - const newIndex = (this.state.focusedIndex + direction + this.itemRefs.length) % this.itemRefs.length; - this.setState({focusedIndex: newIndex}, () => { - this.refocusRef(this.itemRefs[newIndex]); - }); - } - - handleOnClose () { - this.context.cut(this.menuRef); - this.setState({focusedIndex: -1}, () => { - this.refocusRef(this.menuRef); - }); - - // this.props.onClose(); - } - - isExpanded () { - return this.context.isOpenMenu(this.menuRef); - } -} - -BaseMenu.propTypes = { - menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), - depth: PropTypes.number, - // onOpen: PropTypes.func, - // onClose: PropTypes.func, - clearOnItemSelect: PropTypes.bool -}; - -BaseMenu.defaultProps = { - onClose: () => {}, - clearOnItemSelect: false -}; - -export default BaseMenu; diff --git a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx index e3f33b5dcd..301c4682a2 100644 --- a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useRef} from 'react'; import styles from './menu-bar.css'; import classNames from 'classnames'; import PropTypes from 'prop-types'; @@ -7,7 +7,7 @@ import editIcon from './icon--edit.svg'; import {FormattedMessage, defineMessage} from 'react-intl'; import MenuBarMenu from './menu-bar-menu.jsx'; import {MenuItem, MenuSection} from '../menu/menu.jsx'; -import BaseMenu from './base-menu'; +import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; import dropdownCaret from './dropdown-caret.svg'; import DeletionRestorer from '../../containers/deletion-restorer.jsx'; import TurboMode from '../../containers/turbo-mode.jsx'; @@ -19,86 +19,103 @@ const editMenu = defineMessage({ description: 'ARIA label for edit menu' }); -export class EditMenu extends BaseMenu { - constructor (props) { - super(props); +/** + * EditMenu component – the "Edit" dropdown menu in the menu bar. + * + * Handles opening/closing the menu, keyboard navigation, and rendering + * menu items like Restore and Turbo Mode toggles. + * @param {object} props - Component props. + * @param {object} props.intl - React Intl object for formatting messages. + * @param {boolean} [props.isRtl] - Whether the UI is right-to-left. + * @param {(deletedItem: {id: string, [key: string]: unknown}) => string} props.restoreOptionMessage + * Function that returns the label for the restore menu item. + * @param {(handleRestore: () => void) => () => void} props.onRestoreOption + * Function that takes a restore callback and returns a click handler. + * @returns {React.ReactNode} The Edit menu button with dropdown items. + */ +const EditMenu = props => { + const restoreRef = useRef(null); + const turboRef = useRef(null); - this.restoreRef = React.createRef(); - this.turboRef = React.createRef(); - - this.itemRefs = [ - this.restoreRef, - this.turboRef - ]; - } + const itemRefs = [restoreRef, turboRef]; - render () { - return ( -
    + + + + + + - - - - - - - {(handleRestore, {restorable, deletedItem}) => ( + {(handleRestore, {restorable, deletedItem}) => ( + + {props.restoreOptionMessage(deletedItem)} + + )} + + {(toggleTurboMode, {turboMode}) => ( - {this.props.restoreOptionMessage(deletedItem)} + {turboMode ? ( + + ) : ( + + )} - )} - - {(toggleTurboMode, {turboMode}) => ( - - {turboMode ? ( - - ) : ( - - )} - - )} - - -
    - ); - } -} + )} +
    + +
    + ); +}; EditMenu.propTypes = { intl: intlShape.isRequired, diff --git a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx index 559c920f03..6fd34adbc6 100644 --- a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useRef} from 'react'; import styles from './menu-bar.css'; import classNames from 'classnames'; import PropTypes from 'prop-types'; @@ -7,9 +7,9 @@ import fileIcon from './icon--file.svg'; import {FormattedMessage, defineMessage} from 'react-intl'; import MenuBarMenu from './menu-bar-menu.jsx'; import {MenuItem, MenuSection} from '../menu/menu.jsx'; -import BaseMenu from './base-menu'; import SB3Downloader from '../../containers/sb3-downloader.jsx'; import dropdownCaret from './dropdown-caret.svg'; +import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; import sharedMessages from '../../lib/shared-messages'; import intlShape from '../../lib/intlShape.js'; @@ -20,168 +20,191 @@ const fileMenu = defineMessage({ description: 'ARIA label for file menu' }); -export class FileMenu extends BaseMenu { - constructor (props) { - super(props); +/** + * File menu dropdown in the menu bar. + * @param {object} props + * @param {import('react-intl').IntlShape} props.intl - React Intl object. + * @param {boolean} props.isRtl - Whether layout is right-to-left. + * @param {boolean} props.canSave - Whether saving is allowed. + * @param {boolean} props.canCreateCopy - Whether creating a copy is allowed. + * @param {boolean} props.canRemix - Whether remixing is allowed. + * @param {() => void} props.onClickNew - Handler for creating a new project. + * @param {() => void} props.onClickSave - Handler for saving the project. + * @param {() => void} props.onClickSaveAsCopy - Handler for saving a copy. + * @param {() => void} props.onClickRemix - Handler for remixing. + * @param {() => void} props.onStartSelectingFileUpload - Handler for loading from computer. + * @param {(downloadCb: () => void) => () => void} props.getSaveToComputerHandler + * - Returns a click handler that triggers project download. + * @returns {React.ReactElement} + */ +const FileMenu = props => { + const { + intl, + isRtl, + menuRef, + canSave, + canCreateCopy, + canRemix, + onClickNew, + onClickSave, + onClickSaveAsCopy, + onClickRemix, + onStartSelectingFileUpload, + getSaveToComputerHandler + } = props; - this.newProjectRef = React.createRef(); - this.saveRef = React.createRef(); - this.createRef = React.createRef(); - this.remixRef = React.createRef(); - this.loadFromComputerRef = React.createRef(); - this.saveToComputerRef = React.createRef(); - - this.itemRefs = [ - this.newProjectRef, - ...(this.props.canSave ? [this.saveRef] : []), - ...(this.props.canCreateCopy ? [this.createRef] : []), - ...(this.props.canRemix ? [this.remixRef] : []), - this.loadFromComputerRef, - this.saveToComputerRef - ]; - } + const newProjectRef = useRef(null); + const saveRef = useRef(null); + const createRef = useRef(null); + const remixRef = useRef(null); + const loadFromComputerRef = useRef(null); + const saveToComputerRef = useRef(null); + + const itemRefs = [ + newProjectRef, + ...(canSave ? [saveRef] : []), + ...(canCreateCopy ? [createRef] : []), + ...(canRemix ? [remixRef] : []), + loadFromComputerRef, + saveToComputerRef + ]; - render () { - const { - intl, - isRtl, - menuRef, - canSave, - canCreateCopy, - canRemix, - onClickNew, - onClickSave, - onClickSaveAsCopy, - onClickRemix, - onStartSelectingFileUpload, - getSaveToComputerHandler - } = this.props; + const { + isExpanded, + handleKeyPress, + handleKeyPressOpenMenu, + handleOnOpen, + handleOnClose + } = useMenuNavigation({ + menuRef, + itemRefs, + depth: 1 + }); - const saveNowMessage = ( - - ); - const createCopyMessage = ( - - ); - const remixMessage = ( - - ); - const newProjectMessage = ( - - ); - return ( -
    + ); + const createCopyMessage = ( + + ); + const remixMessage = ( + + ); + const newProjectMessage = ( + + ); + return ( +
    + + + + + + - - - - - - + + + {newProjectMessage} + + + {(canSave || canCreateCopy || canRemix) && ( - - {newProjectMessage} - - - {(canSave || canCreateCopy || canRemix) && ( - - {canSave && ( - - {saveNowMessage} - - )} - {canCreateCopy && ( - - {createCopyMessage} - - )} - {canRemix && ( - - {remixMessage} - - )} - - )} - - - {intl.formatMessage(sharedMessages.loadFromComputerTitle)} - - {(className, downloadProjectCallback) => ( + {canSave && ( + + {saveNowMessage} + + )} + {canCreateCopy && ( + + {createCopyMessage} + + )} + {canRemix && ( - + {remixMessage} - )} + )} - -
    - ); - } -} + )} + + + {intl.formatMessage(sharedMessages.loadFromComputerTitle)} + + {(className, downloadProjectCallback) => ( + + + + )} + + +
    + ); +}; FileMenu.propTypes = { menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), @@ -194,7 +217,8 @@ FileMenu.propTypes = { onClickSave: PropTypes.func, onClickSaveAsCopy: PropTypes.func, onClickRemix: PropTypes.func, - onClickNew: PropTypes.func + onClickNew: PropTypes.func, + getSaveToComputerHandler: PropTypes.func }; export default FileMenu; diff --git a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx index 7c47efd370..578e27d1fe 100644 --- a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx @@ -1,7 +1,6 @@ import classNames from 'classnames'; -import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; -import React from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import {FormattedMessage, defineMessage} from 'react-intl'; import {connect} from 'react-redux'; import locales from 'scratch-l10n'; @@ -9,14 +8,13 @@ import locales from 'scratch-l10n'; import check from './check.svg'; import {MenuItem, Submenu} from '../menu/menu.jsx'; import languageIcon from '../language-selector/language-icon.svg'; -import {closeLanguageMenu, openLanguageMenu} from '../../reducers/menus.js'; import {selectLocale} from '../../reducers/locales.js'; +import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; import styles from './settings-menu.css'; import intlShape from '../../lib/intlShape.js'; import dropdownCaret from './dropdown-caret.svg'; -import BaseMenu from './base-menu'; const languageMenu = defineMessage({ id: 'languageMenu.aria.languageMenu', @@ -24,112 +22,113 @@ const languageMenu = defineMessage({ description: 'ARIA label for language menu' }); -class LanguageMenu extends BaseMenu { - constructor (props) { - super(props); - bindAll(this, [ - 'setRef', - 'handleMouseOver' - ]); - - this.itemRefs = Object.keys(locales).map(() => React.createRef()); - } - - componentDidUpdate (prevProps) { - // If the submenu has been toggled open, try scrolling the selected option into view. - if (!prevProps.menuOpen && this.props.menuOpen && this.selectedRef) { - this.selectedRef.scrollIntoView({block: 'center'}); +const LanguageMenu = props => { + const { + intl, + currentLocale, + menuRef, + isRtl, + onChangeLanguage + } = props; + + const itemRefs = React.useMemo( + () => Object.keys(locales).map(() => React.createRef()), + [] + ); + let selectedRef = useRef(null); + + const { + isExpanded, + handleKeyPress, + handleKeyPressOpenMenu, + handleOnOpen + } = useMenuNavigation({ + menuRef, + itemRefs, + depth: 2, + defaultIndexOnOpen: (Object.keys(locales).indexOf(currentLocale)) + }); + + useEffect(() => { + const selectedIndex = Object.keys(locales).indexOf(currentLocale); + if (isExpanded() && selectedIndex >= 0 && itemRefs[selectedIndex]?.current) { + itemRefs[selectedIndex].current.scrollIntoView({block: 'center'}); } - } + }, [currentLocale, isExpanded, itemRefs]); - setRef (component) { - this.selectedRef = component; - } + const setRef = useCallback(component => { + selectedRef = component; + }, []); - handleMouseOver () { + const handleMouseOver = useCallback(() => { // If we are using hover rather than clicks for submenus, scroll the selected option into view - if (!this.props.menuOpen && this.selectedRef) { - this.selectedRef.scrollIntoView({block: 'center'}); + if (isExpanded() && selectedRef) { + selectedRef.scrollIntoView({block: 'center'}); } - } - - handleOnOpen () { - super.handleOnOpen(); - this.refocusItemByIndex(Object.keys(locales).indexOf(this.props.currentLocale)); - } - - render () { - const { - intl, - currentLocale, - menuRef, - isRtl, - onChangeLanguage - } = this.props; - - return ( - -
    - - - - - +
    + + + -
    - - { - Object.keys(locales) - .map((locale, index) => { - const isSelected = currentLocale === locale; - - return ( onChangeLanguage(locale)} - menuRef={this.itemRefs[index]} - onParentKeyPress={this.handleKeyPressOpenMenu} - isSelected={isSelected} - > - - {locales[locale].name} - ); - }) - } - - - ); - } -} + + +
    + + { + Object.keys(locales) + .map((locale, index) => { + const isSelected = currentLocale === locale; + + return ( onChangeLanguage(locale)} + menuRef={itemRefs[index]} + onParentKeyPress={handleKeyPressOpenMenu} + isSelected={isSelected} + > + + {locales[locale].name} + ); + }) + } + +
    + ); +}; LanguageMenu.propTypes = { intl: intlShape, @@ -137,9 +136,7 @@ LanguageMenu.propTypes = { menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), isRtl: PropTypes.bool, menuOpen: PropTypes.bool, - onChangeLanguage: PropTypes.func, - onOpen: PropTypes.func, - onClose: PropTypes.func + onChangeLanguage: PropTypes.func }; const mapStateToProps = state => ({ @@ -151,9 +148,7 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => ({ onChangeLanguage: locale => { dispatch(selectLocale(locale)); - }, - onOpen: () => dispatch(openLanguageMenu()), - onClose: () => dispatch(closeLanguageMenu()) + } }); export default connect( diff --git a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx index 2fbbd2560f..9c3128211d 100644 --- a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx @@ -3,10 +3,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import {FormattedMessage} from 'react-intl'; import {connect} from 'react-redux'; +import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; import check from './check.svg'; import {MenuItem, Submenu} from '../menu/menu.jsx'; -import BaseMenu from './base-menu.jsx'; import styles from './settings-menu.css'; @@ -53,68 +53,73 @@ PreferenceItem.propTypes = { menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}) }; -class PreferenceMenu extends BaseMenu { - constructor (props) { - super(props); +const PreferenceMenu = props => { + const { + itemsMap, + onChange, + defaultMenuIconSrc, + submenuLabel, + selectedItemKey, + isRtl, + menuRef, + ariaLabel + } = props; - this.itemRefs = Object.keys(this.props.itemsMap).map(() => React.createRef()); - } + const itemRefs = Object.keys(itemsMap).map(() => React.createRef()); - render () { - const { - itemsMap, - onChange, - defaultMenuIconSrc, - submenuLabel, - selectedItemKey, - isRtl, - menuRef, - ariaLabel - } = this.props; + const itemKeys = Object.keys(itemsMap); + const selectedItem = itemsMap[selectedItemKey]; - const itemKeys = Object.keys(itemsMap); - const selectedItem = itemsMap[selectedItemKey]; + const { + isExpanded, + handleKeyPress, + handleKeyPressOpenMenu, + handleOnOpen + } = useMenuNavigation({ + menuRef, + itemRefs, + depth: 2 + }); - return ( - -
    - - - - - -
    - - {itemKeys.map((itemKey, index) => ( - onChange(itemKey)} - item={itemsMap[itemKey]} - menuRef={this.itemRefs[index]} - />) - )} - -
    - ); - } + return ( + +
    + + + + + +
    + + {itemKeys.map((itemKey, index) => ( + onChange(itemKey)} + item={itemsMap[itemKey]} + menuRef={itemRefs[index]} + />) + )} + +
    + ); }; PreferenceMenu.propTypes = { diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx index 7a24886ac1..1f5b799d73 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx @@ -1,8 +1,9 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; -import React from 'react'; -import {FormattedMessage, defineMessage, defineMessages} from 'react-intl'; +import React, {useRef, useMemo} from 'react'; +import {FormattedMessage, defineMessages} from 'react-intl'; import {connect} from 'react-redux'; +import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; import LanguageMenu from './language-menu.jsx'; import MenuBarMenu from './menu-bar-menu.jsx'; @@ -20,10 +21,8 @@ import styles from './settings-menu.css'; import dropdownCaret from './dropdown-caret.svg'; import settingsIcon from './icon--settings.svg'; -import BaseMenu from './base-menu.jsx'; import themeIcon from '../../lib/assets/icon--theme.svg'; -import {openColorModeMenu, openThemeMenu} from '../../reducers/menus.js'; import intlShape from '../../lib/intlShape.js'; const ariaMessages = defineMessages({ @@ -46,125 +45,143 @@ const ariaMessages = defineMessages({ const enabledColorModes = [DEFAULT_MODE, HIGH_CONTRAST_MODE]; -class SettingsMenu extends BaseMenu { - constructor (props) { - super(props); - - this.languageRef = React.createRef(); - this.themeRef = React.createRef(); - this.colorRef = React.createRef(); - this.itemRefs = [ - ...(this.props.canChangeLanguage ? [this.languageRef] : []), - ...(this.props.canChangeTheme && this.props.availableThemesLength > 1 ? [this.themeRef] : []), - ...(this.props.canChangeColorMode ? [this.colorRef] : []) - ]; - } - - render () { - const { - canChangeLanguage, - canChangeColorMode, - canChangeTheme, - hasActiveMembership, - intl, - isRtl, - activeColorMode, - onChangeColorMode, - onRequestOpenColorMode, - onRequestOpenTheme, - activeTheme, - onChangeTheme - } = this.props; - - const enabledColorModesMap = Object.keys(colorModeMap).reduce((acc, colorMode) => { - if (enabledColorModes.includes(colorMode)) { - acc[colorMode] = colorModeMap[colorMode]; - } - return acc; - }, {}); - const availableThemesMap = Object.keys(themeMap).reduce((acc, themeKey) => { - const theme = themeMap[themeKey]; - if (theme.isAvailable?.({hasActiveMembership})) { - acc[themeKey] = theme; - } - return acc; - }, {}); - const availableThemesLength = Object.keys(availableThemesMap).length; - - return (
    void} props.onChangeColorMode – Callback to change color mode. + * @param {string} props.activeTheme – Current theme key. + * @param {() => void} props.onChangeTheme – Callback to change theme. + * @returns {React.ReactNode} Settings menu dropdown. + */ +const SettingsMenu = props => { + const { + menuRef, + canChangeLanguage, + canChangeColorMode, + canChangeTheme, + hasActiveMembership, + intl, + isRtl, + activeColorMode, + onChangeColorMode, + activeTheme, + onChangeTheme + } = props; + + const enabledColorModesMap = useMemo(() => Object.keys(colorModeMap).reduce((acc, colorMode) => { + if (enabledColorModes.includes(colorMode)) { + acc[colorMode] = colorModeMap[colorMode]; + } + return acc; + }, {}), []); + const availableThemesMap = useMemo(() => Object.keys(themeMap).reduce((acc, themeKey) => { + const theme = themeMap[themeKey]; + if (theme.isAvailable?.({hasActiveMembership})) { + acc[themeKey] = theme; + } + return acc; + }, {}), [hasActiveMembership]); + const availableThemesLength = useMemo(() => Object.keys(availableThemesMap).length, [availableThemesMap]); + + const languageRef = useRef(null); + const themeRef = useRef(null); + const colorRef = useRef(null); + const itemRefs = [ + ...(canChangeLanguage ? [languageRef] : []), + ...(canChangeTheme && availableThemesLength > 1 ? [themeRef] : []), + ...(canChangeColorMode ? [colorRef] : []) + ]; + + const { + isExpanded, + handleOnOpen, + handleOnClose, + handleKeyPress + } = useMenuNavigation({ + menuRef, + itemRefs, + depth: 1 + }); + + return (
    + + + + + + - - - - - - - - {canChangeLanguage && } - {canChangeTheme && - // TODO: Consider always showing the theme menu, even if there is a single available theme - availableThemesLength > 1 && - } - {canChangeColorMode && + {canChangeLanguage && } + {canChangeTheme && + // TODO: Consider always showing the theme menu, even if there is a single available theme + availableThemesLength > 1 && + } - - -
    ); - } + {canChangeColorMode && } + + +
    ); }; SettingsMenu.propTypes = { + menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), intl: intlShape, canChangeLanguage: PropTypes.bool, canChangeColorMode: PropTypes.bool, @@ -173,10 +190,8 @@ SettingsMenu.propTypes = { isRtl: PropTypes.bool, activeColorMode: PropTypes.string, onChangeColorMode: PropTypes.func, - onRequestOpenColorMode: PropTypes.func, activeTheme: PropTypes.string, - onChangeTheme: PropTypes.func, - onRequestOpenTheme: PropTypes.func + onChangeTheme: PropTypes.func }; const mapStateToProps = state => ({ @@ -185,12 +200,6 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = (dispatch, ownProps) => ({ - onRequestOpenColorMode: () => { - dispatch(openColorModeMenu()); - }, - onRequestOpenTheme: () => { - dispatch(openThemeMenu()); - }, onChangeColorMode: colorMode => { dispatch(setColorMode(colorMode)); ownProps.onClose(); diff --git a/packages/scratch-gui/src/contexts/menu-ref-context.jsx b/packages/scratch-gui/src/contexts/menu-ref-context.jsx index 7c562428c7..a2d2622854 100644 --- a/packages/scratch-gui/src/contexts/menu-ref-context.jsx +++ b/packages/scratch-gui/src/contexts/menu-ref-context.jsx @@ -34,7 +34,7 @@ export class MenuRefProvider extends React.Component { pop () { this.setState(prev => ({ - stack: prev.refStack.slice(0, prev.refStack.length - 1) + refStack: prev.refStack.slice(0, prev.refStack.length - 1) })); } @@ -52,6 +52,7 @@ export class MenuRefProvider extends React.Component { } clear () { + console.log("clearing now"); this.setState({refStack: []}); } diff --git a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx new file mode 100644 index 0000000000..15b96057ce --- /dev/null +++ b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx @@ -0,0 +1,122 @@ +import {useCallback, useContext, useState, useEffect} from 'react'; +import {MenuRefContext} from '../contexts/menu-ref-context'; + +/** + * Provides keyboard navigation and focus management logic for menu components. + * + * This hook encapsulates shared menu behavior such as: + * - opening and closing menus + * - moving focus between menu items with arrow keys + * - handling Escape, Enter, and Tab behavior + * - coordinating open menus via MenuRefContext + * @param {object} params + * Parameters object + * @param {{ current: HTMLElement | null }} params.menuRef + * Ref to the menu trigger or container element. + * @param {Array<{ current: HTMLElement | null }>} params.itemRefs + * Refs for each focusable menu item, in display order. + * @param {number} params.depth + * Nesting depth of the menu (1 = top-level menu). + * @param {number} params.defaultIndexOnOpen + * Default menu item index to open to + * @returns {object} An object containing the focused index, menu state, and keyboard handlers: + * - focusedIndex: number — Index of the currently focused menu item. + * - isExpanded: function() — Returns true if the menu is expanded. + * - handleKeyPress: function(KeyboardEvent) — Handler for key presses on the menu. + * - handleKeyPressOpenMenu: function(KeyboardEvent) — Handler for key presses when the menu is open. + * - handleOnOpen: function() — Function to open the menu. + * - handleOnClose: function() — Function to close the menu. + */ +export default function useMenuNavigation ({ + menuRef, + itemRefs, + depth, + defaultIndexOnOpen = 0 +}) { + const menuContext = useContext(MenuRefContext); + const [focusedIndex, setFocusedIndex] = useState(-1); + + const refocusRef = useCallback(ref => { + if (ref?.current) { + ref.current.focus(); + } + }, []); + + useEffect(() => { + if (focusedIndex >= 0) { + refocusRef(itemRefs[focusedIndex]); + } + }, [focusedIndex]); + + const isExpanded = useCallback( + () => menuContext.isOpenMenu(menuRef), + [menuContext, menuRef] + ); + + const handleOnOpen = useCallback(() => { + if (menuContext.isOpenMenu(menuRef)) return; + + menuContext.push(menuRef, depth); + setFocusedIndex(defaultIndexOnOpen); + }, [menuContext, menuRef, depth]); + + const handleOnClose = useCallback(() => { + setFocusedIndex(-1); + menuContext.cut(menuRef); + refocusRef(menuRef); + }, [menuContext, menuRef, refocusRef]); + + const handleMove = useCallback(direction => { + const nextIndex = + (focusedIndex + direction + itemRefs.length) % + itemRefs.length; + + setFocusedIndex(nextIndex); + }, [focusedIndex, itemRefs, refocusRef]); + + const handleKeyPressOpenMenu = useCallback(e => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + handleMove(1); + } + if (e.key === 'ArrowUp') { + e.preventDefault(); + handleMove(-1); + } + if (e.key === 'ArrowLeft' || e.key === 'Escape') { + e.preventDefault(); + handleOnClose(); + } + }, [handleMove, handleOnClose, menuContext]); + + const handleKeyPress = useCallback(e => { + if (isExpanded() && depth === 1 && e.key === 'Tab') { + handleOnClose(); + menuContext.clear(); + } + + if (menuContext.isTopMenu(menuRef)) { + handleKeyPressOpenMenu(e); + } else if (!isExpanded() && (e.key === ' ' || (e.key === 'ArrowRight' && depth !== 1))) { + e.preventDefault(); + handleOnOpen(); + } + }, [ + depth, + menuContext, + menuRef, + isExpanded, + handleKeyPressOpenMenu, + handleOnOpen, + handleOnClose + ]); + + return { + focusedIndex, + isExpanded, + handleKeyPress, + handleKeyPressOpenMenu, + handleOnOpen, + handleOnClose + }; +} From 287e1a514754c14452c5569db0c1a690f73e1f24 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Thu, 8 Jan 2026 15:52:29 +0200 Subject: [PATCH 18/66] chore: more refactoring --- .../src/components/menu-bar/edit-menu.jsx | 33 ++-- .../src/components/menu-bar/file-menu.jsx | 17 -- .../src/components/menu-bar/language-menu.jsx | 1 + .../src/components/menu-bar/menu-bar.jsx | 27 +-- .../src/components/menu-bar/mode-menu.jsx | 170 +++++++++--------- .../src/contexts/menu-ref-context.jsx | 12 +- .../src/hooks/use-menu-navigation.jsx | 3 +- .../src/lib/sb-file-uploader-hoc.jsx | 8 - packages/scratch-gui/src/reducers/menus.js | 37 +--- 9 files changed, 118 insertions(+), 190 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx index 301c4682a2..30fcd5e1b5 100644 --- a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx @@ -19,21 +19,15 @@ const editMenu = defineMessage({ description: 'ARIA label for edit menu' }); -/** - * EditMenu component – the "Edit" dropdown menu in the menu bar. - * - * Handles opening/closing the menu, keyboard navigation, and rendering - * menu items like Restore and Turbo Mode toggles. - * @param {object} props - Component props. - * @param {object} props.intl - React Intl object for formatting messages. - * @param {boolean} [props.isRtl] - Whether the UI is right-to-left. - * @param {(deletedItem: {id: string, [key: string]: unknown}) => string} props.restoreOptionMessage - * Function that returns the label for the restore menu item. - * @param {(handleRestore: () => void) => () => void} props.onRestoreOption - * Function that takes a restore callback and returns a click handler. - * @returns {React.ReactNode} The Edit menu button with dropdown items. - */ const EditMenu = props => { + const { + menuRef, + intl, + isRtl, + onRestoreOption, + restoreOptionMessage + } = props; + const restoreRef = useRef(null); const turboRef = useRef(null); @@ -46,7 +40,7 @@ const EditMenu = props => { handleOnOpen, handleOnClose } = useMenuNavigation({ - menuRef: props.menuRef, + menuRef, itemRefs, depth: 1 }); @@ -58,10 +52,11 @@ const EditMenu = props => { })} onClick={handleOnOpen} role="button" - aria-label={props.intl.formatMessage(editMenu)} + aria-label={intl.formatMessage(editMenu)} aria-expanded={isExpanded()} tabIndex={0} onKeyDown={handleKeyPress} + ref={menuRef} > @@ -75,18 +70,18 @@ const EditMenu = props => { {(handleRestore, {restorable, deletedItem}) => ( - {props.restoreOptionMessage(deletedItem)} + {restoreOptionMessage(deletedItem)} )} diff --git a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx index 6fd34adbc6..1cbbf40ad0 100644 --- a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx @@ -20,23 +20,6 @@ const fileMenu = defineMessage({ description: 'ARIA label for file menu' }); -/** - * File menu dropdown in the menu bar. - * @param {object} props - * @param {import('react-intl').IntlShape} props.intl - React Intl object. - * @param {boolean} props.isRtl - Whether layout is right-to-left. - * @param {boolean} props.canSave - Whether saving is allowed. - * @param {boolean} props.canCreateCopy - Whether creating a copy is allowed. - * @param {boolean} props.canRemix - Whether remixing is allowed. - * @param {() => void} props.onClickNew - Handler for creating a new project. - * @param {() => void} props.onClickSave - Handler for saving the project. - * @param {() => void} props.onClickSaveAsCopy - Handler for saving a copy. - * @param {() => void} props.onClickRemix - Handler for remixing. - * @param {() => void} props.onStartSelectingFileUpload - Handler for loading from computer. - * @param {(downloadCb: () => void) => () => void} props.getSaveToComputerHandler - * - Returns a click handler that triggers project download. - * @returns {React.ReactElement} - */ const FileMenu = props => { const { intl, diff --git a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx index 578e27d1fe..c4aeb0c1e2 100644 --- a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx @@ -134,6 +134,7 @@ LanguageMenu.propTypes = { intl: intlShape, currentLocale: PropTypes.string, menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), + settingsMenuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), isRtl: PropTypes.bool, menuOpen: PropTypes.bool, onChangeLanguage: PropTypes.func diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx index fe8fcb4f7e..e3fe132550 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx @@ -56,17 +56,9 @@ import { openAccountMenu, closeAccountMenu, accountMenuOpen, - openFileMenu, - closeFileMenu, - openEditMenu, - closeEditMenu, openLoginMenu, closeLoginMenu, - loginMenuOpen, - openModeMenu, - closeModeMenu, - openSettingsMenu, - closeSettingsMenu + loginMenuOpen } from '../../reducers/menus'; import collectMetadata from '../../lib/collect-metadata'; @@ -455,14 +447,11 @@ class MenuBar extends React.Component { hasActiveMembership={this.props.hasActiveMembership} intl={this.props.intl} isRtl={this.props.isRtl} - onClose={this.props.onRequestCloseSettings} onOpen={this.props.onClickSettings} />)} {(this.props.canManageFiles) && ( ({ onOpenDebugModal: () => dispatch(openDebugModal()), onClickAccount: () => dispatch(openAccountMenu()), onRequestCloseAccount: () => dispatch(closeAccountMenu()), - onClickFile: () => dispatch(openFileMenu()), - onRequestCloseFile: () => dispatch(closeFileMenu()), - onClickEdit: () => dispatch(openEditMenu()), - onRequestCloseEdit: () => dispatch(closeEditMenu()), onClickLogin: ownProps.onClickLogin ?? (() => dispatch(openLoginMenu())), onRequestCloseLogin: () => dispatch(closeLoginMenu()), - onClickMode: () => dispatch(openModeMenu()), - onRequestCloseMode: () => dispatch(closeModeMenu()), onRequestOpenAbout: () => dispatch(openAboutMenu()), onRequestCloseAbout: () => dispatch(closeAboutMenu()), - onClickSettings: () => dispatch(openSettingsMenu()), - onRequestCloseSettings: () => dispatch(closeSettingsMenu()), onClickNew: needSave => dispatch(requestNewProject(needSave)), onClickRemix: () => dispatch(remixProject()), onClickSave: () => dispatch(manualUpdateProject()), diff --git a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx index 648da4c124..5f96cdf613 100644 --- a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import {FormattedMessage, defineMessage} from 'react-intl'; import MenuBarMenu from './menu-bar-menu.jsx'; import {MenuItem, MenuSection} from '../menu/menu.jsx'; -import BaseMenu from './base-menu'; +import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; import intlShape from '../../lib/intlShape.js'; const modeMenu = defineMessage({ @@ -15,90 +15,98 @@ const modeMenu = defineMessage({ description: 'ARIA label for mode menu' }); -export class ModeMenu extends BaseMenu { - constructor (props) { - super(props); +const ModeMenu = props => { + const { + intl, + isRtl, + mode2020, + modeNow, + onSetMode, + menuRef + } = props; - this.normalRef = React.createRef(); - this.caturdayRef = React.createRef(); - - this.itemRefs = [ - this.normalRef, - this.caturdayRef - ]; - } + const normalRef = React.createRef(); + const caturdayRef = React.createRef(); + + const itemRefs = [ + normalRef, + caturdayRef + ]; - render () { - const { - intl, - isRtl, - mode2020, - modeNow, - onSetMode - } = this.props; + const { + isExpanded, + handleOnOpen, + handleOnClose, + handleKeyPress, + handleKeyPressOpenMenu + } = useMenuNavigation({ + menuRef, + itemRefs, + depth: 1 + }); - return ( -
    -
    - -
    - - - - - {'✓'} - - {' '} - - - - - {'✓'} - - {' '} - - - - + return ( +
    +
    +
    - ); - } -} + + + + + {'✓'} + + {' '} + + + + + {'✓'} + + {' '} + + + + +
    + ); +}; ModeMenu.propTypes = { intl: intlShape, diff --git a/packages/scratch-gui/src/contexts/menu-ref-context.jsx b/packages/scratch-gui/src/contexts/menu-ref-context.jsx index a2d2622854..2a79675120 100644 --- a/packages/scratch-gui/src/contexts/menu-ref-context.jsx +++ b/packages/scratch-gui/src/contexts/menu-ref-context.jsx @@ -18,7 +18,8 @@ export class MenuRefProvider extends React.Component { 'cut', 'clear', 'isTopMenu', - 'isOpenMenu' + 'isOpenMenu', + 'bottomMenu' ]); } @@ -52,10 +53,14 @@ export class MenuRefProvider extends React.Component { } clear () { - console.log("clearing now"); this.setState({refStack: []}); } + bottomMenu () { + const {refStack} = this.state; + return refStack.length > 0 ? refStack[0] : null; + } + isTopMenu (ref) { const {refStack} = this.state; return refStack.length > 0 && refStack[refStack.length - 1] === ref; @@ -73,7 +78,8 @@ export class MenuRefProvider extends React.Component { cut: this.cut, clear: this.clear, isTopMenu: this.isTopMenu, - isOpenMenu: this.isOpenMenu + isOpenMenu: this.isOpenMenu, + bottomMenu: this.bottomMenu }; return ( diff --git a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx index 15b96057ce..ee59f6055c 100644 --- a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx +++ b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx @@ -117,6 +117,7 @@ export default function useMenuNavigation ({ handleKeyPress, handleKeyPressOpenMenu, handleOnOpen, - handleOnClose + handleOnClose, + refocusRef }; } diff --git a/packages/scratch-gui/src/lib/sb-file-uploader-hoc.jsx b/packages/scratch-gui/src/lib/sb-file-uploader-hoc.jsx index 96872a7a53..2dff58329c 100644 --- a/packages/scratch-gui/src/lib/sb-file-uploader-hoc.jsx +++ b/packages/scratch-gui/src/lib/sb-file-uploader-hoc.jsx @@ -19,9 +19,6 @@ import { openLoadingProject, closeLoadingProject } from '../reducers/modals'; -import { - closeFileMenu -} from '../reducers/menus'; import {getProjectTitleFromFilename} from './sb-file-uploader-utils'; const messages = defineMessages({ @@ -116,7 +113,6 @@ const SBFileUploaderHOC = function (WrappedComponent) { // skips ahead to step 7 this.removeFileObjects(); } - this.props.closeFileMenu(); } } // step 4 is below, in mapDispatchToProps @@ -176,7 +172,6 @@ const SBFileUploaderHOC = function (WrappedComponent) { const { cancelFileUpload, - closeFileMenu: closeFileMenuProp, isLoadingUpload, isShowingWithoutId, loadingState, @@ -206,7 +201,6 @@ const SBFileUploaderHOC = function (WrappedComponent) { SBFileUploaderComponent.propTypes = { canSave: PropTypes.bool, cancelFileUpload: PropTypes.func, - closeFileMenu: PropTypes.func, intl: intlShape.isRequired, isLoadingUpload: PropTypes.bool, isShowingWithoutId: PropTypes.bool, @@ -238,13 +232,11 @@ const SBFileUploaderHOC = function (WrappedComponent) { }; const mapDispatchToProps = (dispatch, ownProps) => ({ cancelFileUpload: loadingState => dispatch(onLoadedProject(loadingState, false, false)), - closeFileMenu: () => dispatch(closeFileMenu()), // transition project state from loading to regular, and close // loading screen and file menu onLoadingFinished: (loadingState, success) => { dispatch(onLoadedProject(loadingState, ownProps.canSave, success)); dispatch(closeLoadingProject()); - dispatch(closeFileMenu()); }, // show project loading screen onLoadingStarted: () => dispatch(openLoadingProject()), diff --git a/packages/scratch-gui/src/reducers/menus.js b/packages/scratch-gui/src/reducers/menus.js index 42387b8cd0..f663ee7e74 100644 --- a/packages/scratch-gui/src/reducers/menus.js +++ b/packages/scratch-gui/src/reducers/menus.js @@ -121,31 +121,10 @@ const openAccountMenu = () => openMenu(MENU_ACCOUNT); const closeAccountMenu = () => closeMenu(MENU_ACCOUNT); const accountMenuOpen = state => state.scratchGui.menus[MENU_ACCOUNT]; -const openEditMenu = () => openMenu(MENU_EDIT); -const closeEditMenu = () => closeMenu(MENU_EDIT); - -const openFileMenu = () => openMenu(MENU_FILE); -const closeFileMenu = () => closeMenu(MENU_FILE); - -const openLanguageMenu = () => openMenu(MENU_LANGUAGE); -const closeLanguageMenu = () => closeMenu(MENU_LANGUAGE); - const openLoginMenu = () => openMenu(MENU_LOGIN); const closeLoginMenu = () => closeMenu(MENU_LOGIN); const loginMenuOpen = state => state.scratchGui.menus[MENU_LOGIN]; -const openModeMenu = () => openMenu(MENU_MODE); -const closeModeMenu = () => closeMenu(MENU_MODE); - -const openSettingsMenu = () => openMenu(MENU_SETTINGS); -const closeSettingsMenu = () => closeMenu(MENU_SETTINGS); - -const openColorModeMenu = () => openMenu(MENU_COLOR_MODE); -const closeColorModeMenu = () => closeMenu(MENU_COLOR_MODE); - -const openThemeMenu = () => openMenu(MENU_THEME); -const closeThemeMenu = () => closeMenu(MENU_THEME); - export { reducer as default, initialState as menuInitialState, @@ -155,21 +134,7 @@ export { openAccountMenu, closeAccountMenu, accountMenuOpen, - openEditMenu, - closeEditMenu, - openFileMenu, - closeFileMenu, - openLanguageMenu, - closeLanguageMenu, openLoginMenu, closeLoginMenu, - loginMenuOpen, - openModeMenu, - closeModeMenu, - openSettingsMenu, - closeSettingsMenu, - openColorModeMenu, - closeColorModeMenu, - openThemeMenu, - closeThemeMenu + loginMenuOpen }; From 19fef13fc7771faca1162caaeb911139df87fa58 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Thu, 8 Jan 2026 15:55:43 +0200 Subject: [PATCH 19/66] chore: removed old menus logic --- packages/scratch-gui/src/reducers/menus.js | 26 +--------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/packages/scratch-gui/src/reducers/menus.js b/packages/scratch-gui/src/reducers/menus.js index f663ee7e74..2d75a9fdfc 100644 --- a/packages/scratch-gui/src/reducers/menus.js +++ b/packages/scratch-gui/src/reducers/menus.js @@ -3,14 +3,7 @@ const CLOSE_MENU = 'scratch-gui/menus/CLOSE_MENU'; const MENU_ABOUT = 'aboutMenu'; const MENU_ACCOUNT = 'accountMenu'; -const MENU_EDIT = 'editMenu'; -const MENU_FILE = 'fileMenu'; -const MENU_LANGUAGE = 'languageMenu'; const MENU_LOGIN = 'loginMenu'; -const MENU_MODE = 'modeMenu'; -const MENU_SETTINGS = 'settingsMenu'; -const MENU_COLOR_MODE = 'colorModeMenu'; -const MENU_THEME = 'themeMenu'; class Menu { constructor (id) { @@ -49,16 +42,6 @@ class Menu { // Structure of nested menus, used for collapsing submenus logic. const rootMenu = new Menu('root') - .addChild( - new Menu(MENU_SETTINGS) - .addChild(new Menu(MENU_LANGUAGE)) - .addChild(new Menu(MENU_COLOR_MODE)) - .addChild(new Menu(MENU_THEME)) - ) - .addChild(new Menu(MENU_FILE)) - .addChild(new Menu(MENU_EDIT)) - .addChild(new Menu(MENU_MODE)) - .addChild(new Menu(MENU_SETTINGS)) .addChild(new Menu(MENU_LOGIN)) .addChild(new Menu(MENU_ACCOUNT)) .addChild(new Menu(MENU_ABOUT)); @@ -66,14 +49,7 @@ const rootMenu = new Menu('root') const initialState = { [MENU_ABOUT]: false, [MENU_ACCOUNT]: false, - [MENU_EDIT]: false, - [MENU_FILE]: false, - [MENU_LANGUAGE]: false, - [MENU_LOGIN]: false, - [MENU_MODE]: false, - [MENU_SETTINGS]: false, - [MENU_COLOR_MODE]: false, - [MENU_THEME]: false + [MENU_LOGIN]: false }; const reducer = function (state, action) { From d2fd053cb7000fdba07823ec13f569fa78c29251 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Thu, 8 Jan 2026 16:07:09 +0200 Subject: [PATCH 20/66] chore: package-lock.json back to original --- package-lock.json | 90 +++++++---------------------------------------- 1 file changed, 13 insertions(+), 77 deletions(-) diff --git a/package-lock.json b/package-lock.json index 786c3e3ed4..7d0b33a943 100644 --- a/package-lock.json +++ b/package-lock.json @@ -162,7 +162,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2488,7 +2487,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2511,7 +2509,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -4499,8 +4496,7 @@ "version": "0.4.1646425229", "resolved": "https://registry.npmjs.org/@mediapipe/face_detection/-/face_detection-0.4.1646425229.tgz", "integrity": "sha512-aeCN+fRAojv9ch3NXorP6r5tcGVLR3/gC1HmtqB0WEZBRXrdP6/3W/sGR0dHr1iT6ueiK95G9PVjbzFosf/hrg==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@microbit/microbit-universal-hex": { "version": "0.2.2", @@ -5017,7 +5013,6 @@ "integrity": "sha512-rYKilwgzQ7/imScn3M9/pFfUf4I1AZEH3KhyJmtPdE2zfaXAn2mFfUy4FbKewzc2We5y/LlKLj36fWJLKC2SIQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^3.0.0", "@octokit/graphql": "^5.0.0", @@ -9361,7 +9356,6 @@ "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-4.22.0.tgz", "integrity": "sha512-H535XtZWnWgNwSzv538czjVlbJebDl5QTMOth4RXr2p/kJ1qSIXE0vZvEtO+5EC9b00SvhplECny2yDewQb/Yg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@tensorflow/tfjs-backend-cpu": "4.22.0", "@types/offscreencanvas": "~2019.3.0", @@ -9380,7 +9374,6 @@ "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-4.22.0.tgz", "integrity": "sha512-PT43MGlnzIo+YfbsjM79Lxk9lOq6uUwZuCc8rrp0hfpLjF6Jv8jS84u2jFb+WpUeuF4K33ZDNx8CjiYrGQ2trQ==", "license": "Apache-2.0", - "peer": true, "peerDependencies": { "@tensorflow/tfjs-core": "4.22.0" } @@ -9390,7 +9383,6 @@ "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-4.22.0.tgz", "integrity": "sha512-LEkOyzbknKFoWUwfkr59vSB68DMJ4cjwwHgicXN0DUi3a0Vh1Er3JQqCI1Hl86GGZQvY8ezVrtDIvqR1ZFW55A==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@types/long": "^4.0.1", "@types/offscreencanvas": "~2019.7.0", @@ -10148,7 +10140,6 @@ "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/linkify-it": "*", "@types/mdurl": "*" @@ -10197,7 +10188,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -10282,7 +10272,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -10294,7 +10283,6 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -10507,7 +10495,6 @@ "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.3", "@typescript-eslint/types": "8.46.3", @@ -11205,7 +11192,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -11319,7 +11305,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -11706,7 +11691,6 @@ "resolved": "https://registry.npmjs.org/arraybuffer-loader/-/arraybuffer-loader-1.0.8.tgz", "integrity": "sha512-CwUVCcxCgcgZUu2w741OV6Xj1tvRVQebq22RCyGXiLgJOJ4e4M/59EPYdtK2MLfIN28t1TDvuh2ojstNq3Kh5g==", "license": "MIT", - "peer": true, "dependencies": { "loader-utils": "^1.1.0" }, @@ -12039,7 +12023,6 @@ "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "find-cache-dir": "^4.0.0", "schema-utils": "^4.0.0" @@ -12660,7 +12643,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -14194,7 +14176,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -14392,7 +14373,6 @@ "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", @@ -14599,7 +14579,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -16243,7 +16222,6 @@ "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -16401,7 +16379,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -16706,7 +16683,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -17246,7 +17222,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -17699,7 +17674,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -22203,7 +22177,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -23939,7 +23912,6 @@ "integrity": "sha512-cG1NtMWO9hWpqRNRR3dSvEQa8bFI6iLlqU2x4kwX51FQjp0qus8T9aBaAO6iGp3DeBrhdwuKxckknohkmfvsFw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "abab": "^2.0.0", "acorn": "^6.0.4", @@ -25442,7 +25414,6 @@ "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -30097,7 +30068,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -31803,7 +31773,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -31886,7 +31855,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -32106,7 +32074,6 @@ "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -32327,7 +32294,6 @@ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -32651,7 +32617,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -32741,7 +32706,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -32996,7 +32960,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.12.1", "@types/hoist-non-react-statics": "^3.3.1", @@ -33089,7 +33052,6 @@ "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-9.0.2.tgz", "integrity": "sha512-+4CCab7z8G8glgJoRjAwocsgsv6VA2w7JPxFWHRc7kvz8mec1/K5LutNC2MG28Mn8mu6+bu04XZxHv5gyfT7xQ==", "license": "MIT", - "peer": true, "dependencies": { "hyphenate-style-name": "^1.0.0", "matchmediaquery": "^0.3.0", @@ -33122,7 +33084,6 @@ "resolved": "https://registry.npmjs.org/react-style-proptype/-/react-style-proptype-3.2.2.tgz", "integrity": "sha512-ywYLSjNkxKHiZOqNlso9PZByNEY+FTyh3C+7uuziK0xFXu9xzdyfHwg4S9iyiRRoPCR4k2LqaBBsWVmSBwCWYQ==", "license": "MIT", - "peer": true, "dependencies": { "prop-types": "^15.5.4" } @@ -33798,7 +33759,6 @@ "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", "license": "Apache-2.0", - "peer": true, "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -34134,7 +34094,6 @@ "integrity": "sha512-Qd9c2p0XKZdgT5AYd+KgAMggJ8ZmCs3JnS9PTMWkyUfteKlfmKtxJbWTHkVakxwXs1Ub7jrRYVeFeF7N0sQxyw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@oxc-project/types": "=0.101.0", "@rolldown/pluginutils": "1.0.0-beta.53" @@ -34459,9 +34418,9 @@ } }, "node_modules/scratch-l10n": { - "version": "6.1.46", - "resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-6.1.46.tgz", - "integrity": "sha512-Tv8j4IbEHUPTyhn2gEWxkuekm+dMZ8vDxUfMsS1HKdD2vQLHqWP7aCQTMM9fCNoWBdYd1rqD2j9LC+j7cJu9/g==", + "version": "6.1.52", + "resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-6.1.52.tgz", + "integrity": "sha512-SeVyt/e23P3tDFGwOxM4czdXYpDpxQJxSaD9Wsea4IY0BCOgEhSWbAtGtvwqAoNO5JIFiVy/rqs34fKneRlhBg==", "license": "AGPL-3.0-only", "dependencies": { "@transifex/api": "7.1.5", @@ -34500,9 +34459,9 @@ } }, "node_modules/scratch-paint": { - "version": "4.1.39", - "resolved": "https://registry.npmjs.org/scratch-paint/-/scratch-paint-4.1.39.tgz", - "integrity": "sha512-/4nyoStsdnUpFglp6SFLbJhxqMCnw4KSAc7hmJZiIswP2zp/klDed2GViyvQlZxW+CzEF1RaIIVySnPPoLcFoA==", + "version": "4.1.44", + "resolved": "https://registry.npmjs.org/scratch-paint/-/scratch-paint-4.1.44.tgz", + "integrity": "sha512-pCLdcDngsEhiozw3fcZ/BXp3xBPGyqe3Uy8OOB/I50Lm7BOhRfBVnmsyeIccIY9AQWZJtMDdQTWcUps74ZA/6A==", "license": "AGPL-3.0-only", "dependencies": { "@scratch/paper": "^0.11.20221201200345", @@ -34568,7 +34527,6 @@ "version": "1.0.252", "resolved": "https://registry.npmjs.org/scratch-render-fonts/-/scratch-render-fonts-1.0.252.tgz", "integrity": "sha512-leYCgtHMIqy36KqjraAiwaPYc9Bjy2L8J+vZ/CEnUE2PVP3z0dDoA4akz42/hk44kpVDzD574Th3SANt+PlLVA==", - "peer": true, "dependencies": { "base64-loader": "^1.0.0" } @@ -34668,8 +34626,7 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/seek-bzip": { "version": "1.0.6", @@ -34733,7 +34690,6 @@ "integrity": "sha512-NMPKdfpXTnPn49FDogMBi36SiBfXkSOJqCkk0E4iWOY1tusvvgBwqUmxTX1kmlT6kIYed9YwNKD1sfPpqa5yaA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^9.0.2", "@semantic-release/error": "^3.0.0", @@ -36409,7 +36365,6 @@ "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", "license": "MIT", - "peer": true, "engines": { "node": ">= 18.12.0" }, @@ -36889,7 +36844,6 @@ "integrity": "sha512-agJabQ9P4iW+CshG8B/5OORwySzU3yaLhCSzHNGSIwvhVlXcN0coMYdBuKLo1kCZM3X2D24pa1tO7NcUqVyNyQ==", "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "@tapjs/processinfo": "^3.1.9", "@tapjs/stack": "4.3.0", @@ -37673,7 +37627,6 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -37937,7 +37890,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -38197,7 +38149,6 @@ "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", @@ -38287,7 +38238,6 @@ "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.0.0", @@ -38347,7 +38297,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -38581,8 +38530,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tslog": { "version": "4.10.2", @@ -38601,7 +38549,6 @@ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -39008,7 +38955,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -39467,7 +39413,6 @@ "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "loader-utils": "^2.0.0", "mime-types": "^2.1.27", @@ -39496,7 +39441,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -39775,7 +39719,6 @@ "integrity": "sha512-5hI5NCJwKBGtzWtdKB3c2fOEpI77Iaa0z4mSzZPU1cJ/OqrGbFafm90edVCd7T9Snz+Sh09TMAv4EQqyVLzuEg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@oxc-project/runtime": "0.101.0", "fdir": "^6.5.0", @@ -39870,7 +39813,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -39884,7 +39826,6 @@ "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", @@ -40089,7 +40030,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -40139,7 +40079,6 @@ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -40922,7 +40861,6 @@ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, "license": "ISC", - "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -41031,7 +40969,6 @@ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -41123,8 +41060,8 @@ "redux-throttle": "0.1.1", "scratch-audio": "2.0.268", "scratch-blocks": "1.3.0", - "scratch-l10n": "6.1.46", - "scratch-paint": "4.1.39", + "scratch-l10n": "6.1.52", + "scratch-paint": "4.1.44", "scratch-render-fonts": "1.0.252", "scratch-storage": "5.0.10", "startaudiocontext": "1.2.1", @@ -41483,7 +41420,7 @@ "jsdoc": "3.6.11", "pngjs": "3.4.0", "scratch-blocks": "1.3.0", - "scratch-l10n": "6.1.46", + "scratch-l10n": "6.1.52", "scratch-render-fonts": "1.0.252", "scratch-semantic-release-config": "4.0.0", "scratch-webpack-configuration": "3.1.0", @@ -41581,7 +41518,6 @@ "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^1.2.0", @@ -41705,4 +41641,4 @@ } } } -} +} \ No newline at end of file From 016f46fc027c2c560070797afd24df6eb54ca6d6 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Thu, 8 Jan 2026 16:16:41 +0200 Subject: [PATCH 21/66] chore: deleting rows --- .../src/components/menu-bar/settings-menu.jsx | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx index 1f5b799d73..ff13ab1abf 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx @@ -45,21 +45,6 @@ const ariaMessages = defineMessages({ const enabledColorModes = [DEFAULT_MODE, HIGH_CONTRAST_MODE]; -/** - * SettingsMenu component – renders the "Settings" dropdown in the menu bar. - * @param {object} props - Props - * @param {object} props.intl – React Intl object for messages. - * @param {boolean} props.canChangeLanguage – Show language menu. - * @param {boolean} props.canChangeColorMode – Show color mode menu. - * @param {boolean} props.canChangeTheme – Show theme menu. - * @param {boolean} props.hasActiveMembership – For theme availability. - * @param {boolean} props.isRtl – Right-to-left layout. - * @param {string} props.activeColorMode – Current color mode. - * @param {() => void} props.onChangeColorMode – Callback to change color mode. - * @param {string} props.activeTheme – Current theme key. - * @param {() => void} props.onChangeTheme – Callback to change theme. - * @returns {React.ReactNode} Settings menu dropdown. - */ const SettingsMenu = props => { const { menuRef, From d75933dd4237b65ed332bfceb7d68011227360e7 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Fri, 9 Jan 2026 10:33:31 +0200 Subject: [PATCH 22/66] chore: refactored some menuRef, itemRef code --- .../src/components/menu-bar/edit-menu.jsx | 7 ++++--- .../src/components/menu-bar/file-menu.jsx | 15 ++++++++------- .../src/components/menu-bar/language-menu.jsx | 6 +++--- .../src/components/menu-bar/mode-menu.jsx | 7 ++++--- .../src/components/menu-bar/preference-menu.jsx | 9 +++++---- .../src/components/menu-bar/settings-menu.jsx | 3 ++- packages/scratch-gui/src/components/menu/menu.jsx | 7 ++++--- packages/scratch-gui/src/lib/prop-types.js | 9 +++++++++ 8 files changed, 39 insertions(+), 24 deletions(-) create mode 100644 packages/scratch-gui/src/lib/prop-types.js diff --git a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx index 30fcd5e1b5..b1556a55e8 100644 --- a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx @@ -12,6 +12,7 @@ import dropdownCaret from './dropdown-caret.svg'; import DeletionRestorer from '../../containers/deletion-restorer.jsx'; import TurboMode from '../../containers/turbo-mode.jsx'; import intlShape from '../../lib/intlShape.js'; +import propTypes from '../../lib/prop-types.js'; const editMenu = defineMessage({ id: 'editMenu.aria.editMenu', @@ -77,7 +78,7 @@ const EditMenu = props => { @@ -88,7 +89,7 @@ const EditMenu = props => { {(toggleTurboMode, {turboMode}) => ( {turboMode ? ( @@ -114,7 +115,7 @@ const EditMenu = props => { EditMenu.propTypes = { intl: intlShape.isRequired, - menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), + menuRef: propTypes.ref, isRtl: PropTypes.bool, restoreOptionMessage: PropTypes.func, onRestoreOption: PropTypes.func diff --git a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx index 1cbbf40ad0..d0cc69c6f7 100644 --- a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx @@ -13,6 +13,7 @@ import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; import sharedMessages from '../../lib/shared-messages'; import intlShape from '../../lib/intlShape.js'; +import propTypes from '../../lib/prop-types.js'; const fileMenu = defineMessage({ id: 'fileMenu.aria.fileMenu', @@ -124,7 +125,7 @@ const FileMenu = props => { {newProjectMessage} @@ -135,7 +136,7 @@ const FileMenu = props => { {canSave && ( {saveNowMessage} @@ -144,7 +145,7 @@ const FileMenu = props => { {canCreateCopy && ( {createCopyMessage} @@ -153,7 +154,7 @@ const FileMenu = props => { {canRemix && ( {remixMessage} @@ -164,7 +165,7 @@ const FileMenu = props => { {intl.formatMessage(sharedMessages.loadFromComputerTitle)} @@ -173,7 +174,7 @@ const FileMenu = props => { { }; FileMenu.propTypes = { - menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), + menuRef: propTypes.ref, intl: intlShape, isRtl: PropTypes.bool, canSave: PropTypes.bool, diff --git a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx index c4aeb0c1e2..68deedfdbd 100644 --- a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx @@ -15,6 +15,7 @@ import styles from './settings-menu.css'; import intlShape from '../../lib/intlShape.js'; import dropdownCaret from './dropdown-caret.svg'; +import propTypes from '../../lib/prop-types.js'; const languageMenu = defineMessage({ id: 'languageMenu.aria.languageMenu', @@ -110,7 +111,7 @@ const LanguageMenu = props => { className={styles.languageMenuItem} // eslint-disable-next-line react/jsx-no-bind onClick={() => onChangeLanguage(locale)} - menuRef={itemRefs[index]} + itemRef={itemRefs[index]} onParentKeyPress={handleKeyPressOpenMenu} isSelected={isSelected} > @@ -133,8 +134,7 @@ const LanguageMenu = props => { LanguageMenu.propTypes = { intl: intlShape, currentLocale: PropTypes.string, - menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), - settingsMenuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), + menuRef: propTypes.ref, isRtl: PropTypes.bool, menuOpen: PropTypes.bool, onChangeLanguage: PropTypes.func diff --git a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx index 5f96cdf613..18cae879b2 100644 --- a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx @@ -9,6 +9,7 @@ import {MenuItem, MenuSection} from '../menu/menu.jsx'; import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; import intlShape from '../../lib/intlShape.js'; +import propTypes from '../../lib/prop-types.js'; const modeMenu = defineMessage({ id: 'modeMenu.aria.modeMenu', defaultMessage: 'Mode menu', @@ -74,7 +75,7 @@ const ModeMenu = props => { @@ -89,7 +90,7 @@ const ModeMenu = props => { @@ -110,7 +111,7 @@ const ModeMenu = props => { ModeMenu.propTypes = { intl: intlShape, - menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), + menuRef: propTypes.ref, onSetMode: PropTypes.func, modeNow: PropTypes.bool, mode2020: PropTypes.bool, diff --git a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx index 9c3128211d..dad29c182b 100644 --- a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx @@ -11,6 +11,7 @@ import {MenuItem, Submenu} from '../menu/menu.jsx'; import styles from './settings-menu.css'; import dropdownCaret from './dropdown-caret.svg'; +import propTypes from '../../lib/prop-types.js'; const intlMessageShape = PropTypes.shape({ defaultMessage: PropTypes.string, @@ -25,7 +26,7 @@ const PreferenceItem = props => {
    @@ -50,7 +51,7 @@ PreferenceItem.propTypes = { label: intlMessageShape.isRequired }), onParentKeyPress: PropTypes.func, - menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}) + itemRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}) }; const PreferenceMenu = props => { @@ -114,7 +115,7 @@ const PreferenceMenu = props => { // eslint-disable-next-line react/jsx-no-bind onClick={() => onChange(itemKey)} item={itemsMap[itemKey]} - menuRef={itemRefs[index]} + itemRef={itemRefs[index]} />) )} @@ -133,7 +134,7 @@ PreferenceMenu.propTypes = { submenuLabel: intlMessageShape.isRequired, selectedItemKey: PropTypes.string, isRtl: PropTypes.bool, - menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}) + menuRef: propTypes.ref }; const mapStateToProps = state => ({ diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx index ff13ab1abf..125616dca5 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx @@ -24,6 +24,7 @@ import settingsIcon from './icon--settings.svg'; import themeIcon from '../../lib/assets/icon--theme.svg'; import intlShape from '../../lib/intlShape.js'; +import propTypes from '../../lib/prop-types.js'; const ariaMessages = defineMessages({ settingsMenu: { @@ -166,7 +167,7 @@ const SettingsMenu = props => { }; SettingsMenu.propTypes = { - menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), + menuRef: propTypes.ref, intl: intlShape, canChangeLanguage: PropTypes.bool, canChangeColorMode: PropTypes.bool, diff --git a/packages/scratch-gui/src/components/menu/menu.jsx b/packages/scratch-gui/src/components/menu/menu.jsx index de988c6285..34589998c7 100644 --- a/packages/scratch-gui/src/components/menu/menu.jsx +++ b/packages/scratch-gui/src/components/menu/menu.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import styles from './menu.css'; +import propTypes from '../../lib/prop-types'; const MenuComponent = ({ className = '', @@ -65,7 +66,7 @@ const MenuItem = ({ isSelected = false, isDisabled = false, onClick, - menuRef, + itemRef, ariaLabel, ariaRole, onParentKeyPress @@ -79,7 +80,7 @@ const MenuItem = ({ )} onClick={onClick} tabIndex={-1} - ref={menuRef} + ref={itemRef} aria-label={ariaLabel} aria-selected={isSelected} aria-disabled={isDisabled} @@ -91,7 +92,7 @@ const MenuItem = ({ ); MenuItem.propTypes = { - menuRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), + itemRef: propTypes.ref, ariaLabel: PropTypes.string, ariaRole: PropTypes.string, children: PropTypes.node, diff --git a/packages/scratch-gui/src/lib/prop-types.js b/packages/scratch-gui/src/lib/prop-types.js new file mode 100644 index 0000000000..4b65abd480 --- /dev/null +++ b/packages/scratch-gui/src/lib/prop-types.js @@ -0,0 +1,9 @@ +import PropTypes from 'prop-types'; + +const propTypes = { + ref: PropTypes.shape({ + current: PropTypes.instanceOf(Element) + }) +}; + +export default propTypes; From e186ba95a10e28bd888f3cb7adce667aa003b148 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Fri, 9 Jan 2026 10:36:05 +0200 Subject: [PATCH 23/66] chore: passing down remix message --- .../src/components/menu-bar/file-menu.jsx | 13 ++++--------- .../src/components/menu-bar/menu-bar.jsx | 1 + 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx index d0cc69c6f7..65053987d3 100644 --- a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx @@ -34,7 +34,8 @@ const FileMenu = props => { onClickSaveAsCopy, onClickRemix, onStartSelectingFileUpload, - getSaveToComputerHandler + getSaveToComputerHandler, + remixMessage } = props; const newProjectRef = useRef(null); @@ -79,13 +80,6 @@ const FileMenu = props => { id="gui.menuBar.saveAsCopy" /> ); - const remixMessage = ( - - ); const newProjectMessage = ( )} Date: Fri, 9 Jan 2026 12:18:46 +0200 Subject: [PATCH 24/66] chore: added some isRequired-s --- .../src/components/menu-bar/edit-menu.jsx | 15 +++++-- .../src/components/menu-bar/file-menu.jsx | 43 +++++++++++++------ .../src/components/menu-bar/language-menu.jsx | 5 +-- .../src/components/menu-bar/menu-bar.jsx | 1 - .../src/components/menu-bar/mode-menu.jsx | 20 ++++++--- .../components/menu-bar/preference-menu.jsx | 4 +- .../src/components/menu-bar/settings-menu.jsx | 14 +++--- 7 files changed, 65 insertions(+), 37 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx index b1556a55e8..a667264dd9 100644 --- a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx @@ -2,6 +2,7 @@ import React, {useRef} from 'react'; import styles from './menu-bar.css'; import classNames from 'classnames'; import PropTypes from 'prop-types'; +import {connect} from 'react-redux'; import editIcon from './icon--edit.svg'; import {FormattedMessage, defineMessage} from 'react-intl'; @@ -115,10 +116,16 @@ const EditMenu = props => { EditMenu.propTypes = { intl: intlShape.isRequired, - menuRef: propTypes.ref, + menuRef: propTypes.ref.isRequired, isRtl: PropTypes.bool, - restoreOptionMessage: PropTypes.func, - onRestoreOption: PropTypes.func + restoreOptionMessage: PropTypes.func.isRequired, + onRestoreOption: PropTypes.func.isRequired }; -export default EditMenu; +const mapStateToProps = state => ({ + isRtl: state.locales.isRtl +}); + +export default connect( + mapStateToProps +)(EditMenu); diff --git a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx index 65053987d3..8ed2e15f30 100644 --- a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx @@ -2,6 +2,7 @@ import React, {useRef} from 'react'; import styles from './menu-bar.css'; import classNames from 'classnames'; import PropTypes from 'prop-types'; +import {connect} from 'react-redux'; import fileIcon from './icon--file.svg'; import {FormattedMessage, defineMessage} from 'react-intl'; @@ -15,6 +16,16 @@ import sharedMessages from '../../lib/shared-messages'; import intlShape from '../../lib/intlShape.js'; import propTypes from '../../lib/prop-types.js'; +import { + autoUpdateProject, + getIsUpdating, + getIsShowingProject, + manualUpdateProject, + requestNewProject, + remixProject, + saveProjectAsCopy +} from '../../reducers/project-state'; + const fileMenu = defineMessage({ id: 'fileMenu.aria.fileMenu', defaultMessage: 'File menu', @@ -185,19 +196,25 @@ const FileMenu = props => { }; FileMenu.propTypes = { - menuRef: propTypes.ref, - intl: intlShape, + menuRef: propTypes.ref.isRequired, + intl: intlShape.isRequired, isRtl: PropTypes.bool, - canSave: PropTypes.bool, - canCreateCopy: PropTypes.bool, - canRemix: PropTypes.bool, - onStartSelectingFileUpload: PropTypes.func, - onClickSave: PropTypes.func, - onClickSaveAsCopy: PropTypes.func, - onClickRemix: PropTypes.func, - onClickNew: PropTypes.func, - getSaveToComputerHandler: PropTypes.func, - remixMessage: PropTypes.node + canSave: PropTypes.bool.isRequired, + canCreateCopy: PropTypes.bool.isRequired, + canRemix: PropTypes.bool.isRequired, + onStartSelectingFileUpload: PropTypes.func.isRequired, + onClickSave: PropTypes.func.isRequired, + onClickSaveAsCopy: PropTypes.func.isRequired, + onClickRemix: PropTypes.func.isRequired, + onClickNew: PropTypes.func.isRequired, + getSaveToComputerHandler: PropTypes.func.isRequired, + remixMessage: PropTypes.node.isRequired }; -export default FileMenu; +const mapStateToProps = state => ({ + isRtl: state.locales.isRtl +}); + +export default connect( + mapStateToProps +)(FileMenu); diff --git a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx index 68deedfdbd..1de6cb8d47 100644 --- a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx @@ -132,11 +132,10 @@ const LanguageMenu = props => { }; LanguageMenu.propTypes = { - intl: intlShape, + menuRef: propTypes.ref.isRequired, + intl: intlShape.isRequired, currentLocale: PropTypes.string, - menuRef: propTypes.ref, isRtl: PropTypes.bool, - menuOpen: PropTypes.bool, onChangeLanguage: PropTypes.func }; diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx index 44effd2be4..3a9b8d836d 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx @@ -822,7 +822,6 @@ MenuBar.propTypes = { onRequestCloseFile: PropTypes.func, onRequestCloseLogin: PropTypes.func, onRequestCloseMode: PropTypes.func, - // onRequestCloseSettings: PropTypes.func, onRequestOpenAbout: PropTypes.func, onSeeCommunity: PropTypes.func, onSetTimeTravelMode: PropTypes.func, diff --git a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx index 18cae879b2..ec158891d8 100644 --- a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx @@ -2,6 +2,7 @@ import React from 'react'; import styles from './menu-bar.css'; import classNames from 'classnames'; import PropTypes from 'prop-types'; +import {connect} from 'react-redux'; import {FormattedMessage, defineMessage} from 'react-intl'; import MenuBarMenu from './menu-bar-menu.jsx'; @@ -110,13 +111,18 @@ const ModeMenu = props => { }; ModeMenu.propTypes = { - intl: intlShape, - menuRef: propTypes.ref, - onSetMode: PropTypes.func, - modeNow: PropTypes.bool, - mode2020: PropTypes.bool, + intl: intlShape.isRequired, + menuRef: propTypes.ref.isRequired, + onSetMode: PropTypes.func.isRequired, + modeNow: PropTypes.bool.isRequired, + mode2020: PropTypes.bool.isRequired, isRtl: PropTypes.bool - }; -export default ModeMenu; +const mapStateToProps = state => ({ + isRtl: state.locales.isRtl +}); + +export default connect( + mapStateToProps +)(ModeMenu); diff --git a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx index dad29c182b..48efbf0635 100644 --- a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx @@ -125,6 +125,7 @@ const PreferenceMenu = props => { PreferenceMenu.propTypes = { ariaLabel: PropTypes.string, + menuRef: propTypes.ref.isRequired, itemsMap: PropTypes.objectOf(PropTypes.shape({ icon: PropTypes.string, label: intlMessageShape.isRequired @@ -133,8 +134,7 @@ PreferenceMenu.propTypes = { defaultMenuIconSrc: PropTypes.string, submenuLabel: intlMessageShape.isRequired, selectedItemKey: PropTypes.string, - isRtl: PropTypes.bool, - menuRef: propTypes.ref + isRtl: PropTypes.bool }; const mapStateToProps = state => ({ diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx index 125616dca5..b3d7da6237 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx @@ -167,13 +167,13 @@ const SettingsMenu = props => { }; SettingsMenu.propTypes = { - menuRef: propTypes.ref, - intl: intlShape, - canChangeLanguage: PropTypes.bool, - canChangeColorMode: PropTypes.bool, - canChangeTheme: PropTypes.bool, - hasActiveMembership: PropTypes.bool, - isRtl: PropTypes.bool, + menuRef: propTypes.ref.isRequired, + intl: intlShape.isRequired, + canChangeLanguage: PropTypes.bool.isRequired, + canChangeColorMode: PropTypes.bool.isRequired, + canChangeTheme: PropTypes.bool.isRequired, + hasActiveMembership: PropTypes.bool.isRequired, + isRtl: PropTypes.bool.isRequired, activeColorMode: PropTypes.string, onChangeColorMode: PropTypes.func, activeTheme: PropTypes.string, From fa754844d69a768fe0de729f65df807591e2d364 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Fri, 9 Jan 2026 14:41:05 +0200 Subject: [PATCH 25/66] chore: brought some elements inside file menu --- .../src/components/menu-bar/file-menu.jsx | 20 ++++++++----- .../src/components/menu-bar/menu-bar.jsx | 30 +------------------ 2 files changed, 13 insertions(+), 37 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx index 8ed2e15f30..b859d2aa93 100644 --- a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx @@ -17,11 +17,7 @@ import intlShape from '../../lib/intlShape.js'; import propTypes from '../../lib/prop-types.js'; import { - autoUpdateProject, - getIsUpdating, - getIsShowingProject, manualUpdateProject, - requestNewProject, remixProject, saveProjectAsCopy } from '../../reducers/project-state'; @@ -98,6 +94,7 @@ const FileMenu = props => { id="gui.menuBar.new" /> ); + return (
    ({ isRtl: state.locales.isRtl }); +const mapDispatchToProps = dispatch => ({ + onClickRemix: () => dispatch(remixProject()), + onClickSave: () => dispatch(manualUpdateProject()), + onClickSaveAsCopy: () => dispatch(saveProjectAsCopy()) +}); + export default connect( - mapStateToProps + mapStateToProps, + mapDispatchToProps )(FileMenu); diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx index 3a9b8d836d..ae7c759c11 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx @@ -177,9 +177,6 @@ class MenuBar extends React.Component { super(props); bindAll(this, [ 'handleClickNew', - 'handleClickRemix', - 'handleClickSave', - 'handleClickSaveAsCopy', 'handleClickSeeCommunity', 'handleClickShare', 'handleSetMode', @@ -209,23 +206,9 @@ class MenuBar extends React.Component { const readyToReplaceProject = this.props.confirmReadyToReplaceProject( this.props.intl.formatMessage(sharedMessages.replaceProjectWarning) ); - this.props.onRequestCloseFile(); if (readyToReplaceProject) { this.props.onClickNew(this.props.canSave && this.props.canCreateNew); } - this.props.onRequestCloseFile(); - } - handleClickRemix () { - this.props.onClickRemix(); - this.props.onRequestCloseFile(); - } - handleClickSave () { - this.props.onClickSave(); - this.props.onRequestCloseFile(); - } - handleClickSaveAsCopy () { - this.props.onClickSaveAsCopy(); - this.props.onRequestCloseFile(); } handleClickSeeCommunity (waitForUpdate) { if (this.props.shouldSaveBeforeTransition()) { @@ -281,7 +264,6 @@ class MenuBar extends React.Component { handleRestoreOption (restoreFun) { return () => { restoreFun(); - this.props.onRequestCloseEdit(); }; } handleKeyPress (event) { @@ -298,7 +280,6 @@ class MenuBar extends React.Component { } getSaveToComputerHandler (downloadProjectCallback) { return () => { - this.props.onRequestCloseFile(); downloadProjectCallback(); if (this.props.onProjectTelemetryEvent) { const metadata = collectMetadata(this.props.vm, this.props.projectTitle, this.props.locale); @@ -453,9 +434,6 @@ class MenuBar extends React.Component { menuRef={this.fileRef} depth={1} onStartSelectingFileUpload={this.props.onStartSelectingFileUpload} - onClickSave={this.handleClickSave} - onClickSaveAsCopy={this.handleClickSaveAsCopy} - onClickRemix={this.handleClickRemix} onClickNew={this.handleClickNew} getSaveToComputerHandler={this.getSaveToComputerHandler} canSave={this.props.canSave} @@ -818,10 +796,7 @@ MenuBar.propTypes = { onProjectTelemetryEvent: PropTypes.func, onRequestCloseAbout: PropTypes.func, onRequestCloseAccount: PropTypes.func, - onRequestCloseEdit: PropTypes.func, - onRequestCloseFile: PropTypes.func, onRequestCloseLogin: PropTypes.func, - onRequestCloseMode: PropTypes.func, onRequestOpenAbout: PropTypes.func, onSeeCommunity: PropTypes.func, onSetTimeTravelMode: PropTypes.func, @@ -902,14 +877,11 @@ const mapDispatchToProps = (dispatch, ownProps) => ({ onOpenDebugModal: () => dispatch(openDebugModal()), onClickAccount: () => dispatch(openAccountMenu()), onRequestCloseAccount: () => dispatch(closeAccountMenu()), + onClickNew: needSave => dispatch(requestNewProject(needSave)), onClickLogin: ownProps.onClickLogin ?? (() => dispatch(openLoginMenu())), onRequestCloseLogin: () => dispatch(closeLoginMenu()), onRequestOpenAbout: () => dispatch(openAboutMenu()), onRequestCloseAbout: () => dispatch(closeAboutMenu()), - onClickNew: needSave => dispatch(requestNewProject(needSave)), - onClickRemix: () => dispatch(remixProject()), - onClickSave: () => dispatch(manualUpdateProject()), - onClickSaveAsCopy: () => dispatch(saveProjectAsCopy()), onSeeCommunity: ownProps.onSeeCommunity ?? (() => dispatch(setPlayer(true))), onSetTimeTravelMode: mode => dispatch(setTimeTravel(mode)) }); From 8626a1a51a549cd07a43399c1c175ae876dab214 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Fri, 9 Jan 2026 14:59:13 +0200 Subject: [PATCH 26/66] chore: fixed some aria labels --- .../src/components/menu-bar/edit-menu.jsx | 2 +- .../src/components/menu-bar/file-menu.jsx | 2 +- .../src/components/menu-bar/language-menu.jsx | 2 +- .../src/components/menu-bar/menu-bar.jsx | 19 ++++++++----------- .../src/components/menu-bar/mode-menu.jsx | 2 +- .../src/components/menu-bar/settings-menu.jsx | 6 +++--- 6 files changed, 15 insertions(+), 18 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx index a667264dd9..adaf9c284a 100644 --- a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx @@ -16,7 +16,7 @@ import intlShape from '../../lib/intlShape.js'; import propTypes from '../../lib/prop-types.js'; const editMenu = defineMessage({ - id: 'editMenu.aria.editMenu', + id: 'gui.aria.editMenu', defaultMessage: 'Edit menu', description: 'ARIA label for edit menu' }); diff --git a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx index b859d2aa93..a4581724c3 100644 --- a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx @@ -23,7 +23,7 @@ import { } from '../../reducers/project-state'; const fileMenu = defineMessage({ - id: 'fileMenu.aria.fileMenu', + id: 'gui.aria.fileMenu', defaultMessage: 'File menu', description: 'ARIA label for file menu' }); diff --git a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx index 1de6cb8d47..9e615a3ce6 100644 --- a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx @@ -18,7 +18,7 @@ import dropdownCaret from './dropdown-caret.svg'; import propTypes from '../../lib/prop-types.js'; const languageMenu = defineMessage({ - id: 'languageMenu.aria.languageMenu', + id: 'gui.aria.languageMenu', defaultMessage: 'Language menu', description: 'ARIA label for language menu' }); diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx index ae7c759c11..a6a1cf2249 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx @@ -44,10 +44,7 @@ import { autoUpdateProject, getIsUpdating, getIsShowingProject, - manualUpdateProject, - requestNewProject, - remixProject, - saveProjectAsCopy + requestNewProject } from '../../reducers/project-state'; import { openAboutMenu, @@ -89,17 +86,17 @@ const ariaMessages = defineMessages({ tutorials: { id: 'gui.menuBar.tutorialsLibrary', defaultMessage: 'Tutorials', - description: 'accessibility text for the tutorials button' + description: 'ARIA text for the tutorials button' }, debug: { id: 'gui.menuBar.debug', defaultMessage: 'Debug', - description: 'accessibility text for the debug button' + description: 'ARIA text for the debug button' }, - goHome: { - id: 'gui.menuBar.goHome', - defaultMessage: 'Go home', - description: 'accessibility text for the go home button' + home: { + id: 'gui.menuBar.home', + defaultMessage: 'Home', + description: 'ARIA text for the home button' } }); @@ -406,7 +403,7 @@ class MenuBar extends React.Component {
    Scratch Date: Fri, 9 Jan 2026 16:22:24 +0200 Subject: [PATCH 27/66] chore: context is function instead of class now --- .../src/contexts/menu-ref-context.jsx | 147 ++++++++---------- 1 file changed, 65 insertions(+), 82 deletions(-) diff --git a/packages/scratch-gui/src/contexts/menu-ref-context.jsx b/packages/scratch-gui/src/contexts/menu-ref-context.jsx index 2a79675120..75be03f24d 100644 --- a/packages/scratch-gui/src/contexts/menu-ref-context.jsx +++ b/packages/scratch-gui/src/contexts/menu-ref-context.jsx @@ -1,94 +1,77 @@ -import React from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import PropTypes from 'prop-types'; -import bindAll from 'lodash.bindall'; export const MenuRefContext = React.createContext(null); -export class MenuRefProvider extends React.Component { - constructor (props) { - super(props); +export const MenuRefProvider = ({children}) => { + const [refStack, setRefStack] = useState([]); - this.state = { - refStack: [] - }; - - bindAll(this, [ - 'push', - 'pop', - 'cut', - 'clear', - 'isTopMenu', - 'isOpenMenu', - 'bottomMenu' - ]); - } - - push (ref, depth) { - if (depth <= this.state.refStack.length) { - this.cut(this.state.refStack[depth - 1]); - } - - this.setState(prev => ({ - refStack: [...prev.refStack, ref] - })); - } - - pop () { - this.setState(prev => ({ - refStack: prev.refStack.slice(0, prev.refStack.length - 1) - })); - } - - cut (ref) { - this.setState(prev => { - const refs = prev.refStack; - const index = refs.indexOf(ref); - - if (index === -1) return {refStack: refs}; - - return { - refStack: refs.slice(0, index) - }; + const cut = useCallback(ref => { + setRefStack(prev => { + const index = prev.indexOf(ref); + if (index === -1) return prev; + return prev.slice(0, index); }); - } - - clear () { - this.setState({refStack: []}); - } - - bottomMenu () { - const {refStack} = this.state; - return refStack.length > 0 ? refStack[0] : null; - } + }, []); - isTopMenu (ref) { - const {refStack} = this.state; - return refStack.length > 0 && refStack[refStack.length - 1] === ref; - } + const push = useCallback((ref, depth) => { + setRefStack(prev => { + let next = prev; - isOpenMenu (ref) { - return this.state.refStack.includes(ref); - } + if (depth <= prev.length) { + const cutRef = prev[depth - 1]; + const index = prev.indexOf(cutRef); + if (index !== -1) { + next = prev.slice(0, index); + } + } - render () { - const value = { - refStack: this.state.refStack, - push: this.push, - pop: this.pop, - cut: this.cut, - clear: this.clear, - isTopMenu: this.isTopMenu, - isOpenMenu: this.isOpenMenu, - bottomMenu: this.bottomMenu - }; - - return ( - - {this.props.children} - - ); - } -} + return [...next, ref]; + }); + }, []); + + + const pop = useCallback(() => { + setRefStack(prev => prev.slice(0, prev.length - 1)); + }, []); + + const clear = useCallback(() => { + setRefStack([]); + }, []); + + const bottomMenu = useMemo(() => (refStack.length > 0 ? refStack[0] : null), [refStack]); + + const isTopMenu = useCallback(ref => (refStack.length > 0 && + refStack[refStack.length - 1] === ref), [refStack]); + + const isOpenMenu = useCallback(ref => (refStack.includes(ref)), [refStack]); + + const value = useMemo(() => ({ + refStack, + push, + pop, + cut, + clear, + isTopMenu, + isOpenMenu, + bottomMenu + }), [ + refStack, + push, + pop, + cut, + clear, + isTopMenu, + isOpenMenu, + bottomMenu + ]); + + return ( + + {children} + + ); +}; MenuRefProvider.propTypes = { children: PropTypes.node From dbaa015fb2810277e8bf9001fb3dd7fc5e8baab7 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 12 Jan 2026 09:06:38 +0200 Subject: [PATCH 28/66] chore: more refactoring and semantic improvements --- .../src/components/menu-bar/edit-menu.jsx | 11 ++-- .../src/components/menu-bar/file-menu.jsx | 11 ++-- .../src/components/menu-bar/language-menu.jsx | 16 ++--- .../src/components/menu-bar/menu-bar.css | 6 ++ .../src/components/menu-bar/menu-bar.jsx | 13 ++-- .../src/components/menu-bar/mode-menu.jsx | 21 ++++--- .../components/menu-bar/preference-menu.jsx | 5 +- .../src/components/menu-bar/settings-menu.css | 12 +++- .../src/components/menu-bar/settings-menu.jsx | 12 ++-- .../src/contexts/menu-ref-context.jsx | 62 +++++++++++++------ .../src/hooks/use-menu-navigation.jsx | 31 +++++++--- 11 files changed, 117 insertions(+), 83 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx index adaf9c284a..1f7e7f5d96 100644 --- a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx @@ -5,14 +5,13 @@ import PropTypes from 'prop-types'; import {connect} from 'react-redux'; import editIcon from './icon--edit.svg'; -import {FormattedMessage, defineMessage} from 'react-intl'; +import {useIntl, FormattedMessage, defineMessage} from 'react-intl'; import MenuBarMenu from './menu-bar-menu.jsx'; import {MenuItem, MenuSection} from '../menu/menu.jsx'; import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; import dropdownCaret from './dropdown-caret.svg'; import DeletionRestorer from '../../containers/deletion-restorer.jsx'; import TurboMode from '../../containers/turbo-mode.jsx'; -import intlShape from '../../lib/intlShape.js'; import propTypes from '../../lib/prop-types.js'; const editMenu = defineMessage({ @@ -24,11 +23,11 @@ const editMenu = defineMessage({ const EditMenu = props => { const { menuRef, - intl, isRtl, onRestoreOption, restoreOptionMessage } = props; + const intl = useIntl(); const restoreRef = useRef(null); const turboRef = useRef(null); @@ -48,12 +47,11 @@ const EditMenu = props => { }); return ( -
    { )} -
    + ); }; EditMenu.propTypes = { - intl: intlShape.isRequired, menuRef: propTypes.ref.isRequired, isRtl: PropTypes.bool, restoreOptionMessage: PropTypes.func.isRequired, diff --git a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx index a4581724c3..094be3f874 100644 --- a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx @@ -5,7 +5,7 @@ import PropTypes from 'prop-types'; import {connect} from 'react-redux'; import fileIcon from './icon--file.svg'; -import {FormattedMessage, defineMessage} from 'react-intl'; +import {useIntl, FormattedMessage, defineMessage} from 'react-intl'; import MenuBarMenu from './menu-bar-menu.jsx'; import {MenuItem, MenuSection} from '../menu/menu.jsx'; import SB3Downloader from '../../containers/sb3-downloader.jsx'; @@ -13,7 +13,6 @@ import dropdownCaret from './dropdown-caret.svg'; import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; import sharedMessages from '../../lib/shared-messages'; -import intlShape from '../../lib/intlShape.js'; import propTypes from '../../lib/prop-types.js'; import { @@ -30,7 +29,6 @@ const fileMenu = defineMessage({ const FileMenu = props => { const { - intl, isRtl, menuRef, canSave, @@ -44,6 +42,7 @@ const FileMenu = props => { getSaveToComputerHandler, remixMessage } = props; + const intl = useIntl(); const newProjectRef = useRef(null); const saveRef = useRef(null); @@ -96,14 +95,13 @@ const FileMenu = props => { ); return ( -
    { )} -
    + ); }; FileMenu.propTypes = { menuRef: propTypes.ref.isRequired, - intl: intlShape.isRequired, isRtl: PropTypes.bool, canSave: PropTypes.bool.isRequired, canCreateCopy: PropTypes.bool.isRequired, diff --git a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx index 9e615a3ce6..e8f8f49ce7 100644 --- a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx @@ -1,7 +1,7 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useRef} from 'react'; -import {FormattedMessage, defineMessage} from 'react-intl'; +import {useIntl, FormattedMessage, defineMessage} from 'react-intl'; import {connect} from 'react-redux'; import locales from 'scratch-l10n'; @@ -12,7 +12,6 @@ import {selectLocale} from '../../reducers/locales.js'; import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; import styles from './settings-menu.css'; -import intlShape from '../../lib/intlShape.js'; import dropdownCaret from './dropdown-caret.svg'; import propTypes from '../../lib/prop-types.js'; @@ -25,17 +24,14 @@ const languageMenu = defineMessage({ const LanguageMenu = props => { const { - intl, currentLocale, menuRef, isRtl, onChangeLanguage } = props; + const intl = useIntl(); - const itemRefs = React.useMemo( - () => Object.keys(locales).map(() => React.createRef()), - [] - ); + const itemRefs = React.useMemo(() => Object.keys(locales).map(() => React.createRef()), []); let selectedRef = useRef(null); const { @@ -70,14 +66,13 @@ const LanguageMenu = props => { return ( -
    @@ -96,7 +91,7 @@ const LanguageMenu = props => { className={styles.expandCaret} src={dropdownCaret} /> -
    + { LanguageMenu.propTypes = { menuRef: propTypes.ref.isRequired, - intl: intlShape.isRequired, currentLocale: PropTypes.string, isRtl: PropTypes.bool, onChangeLanguage: PropTypes.func diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.css b/packages/scratch-gui/src/components/menu-bar/menu-bar.css index 68063fcf47..7aa3610348 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.css +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.css @@ -62,6 +62,12 @@ align-items: center; white-space: nowrap; height: $menu-bar-height; + + background: none; + border: none; + font: inherit; + text-align: inherit; + cursor: pointer; } .menu-bar-item.hoverable { diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx index a6a1cf2249..8bf5a5ed6a 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx @@ -423,7 +423,6 @@ class MenuBar extends React.Component { canChangeColorMode={this.props.canChangeColorMode} canChangeTheme={this.props.canChangeTheme} hasActiveMembership={this.props.hasActiveMembership} - intl={this.props.intl} isRtl={this.props.isRtl} onOpen={this.props.onClickSettings} />)} @@ -443,7 +442,6 @@ class MenuBar extends React.Component { )}
    @@ -533,9 +530,8 @@ class MenuBar extends React.Component {
    -
    -
    -
    +
    +
    diff --git a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx index 45828758a8..7586436c83 100644 --- a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx @@ -4,13 +4,18 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; -import {FormattedMessage, defineMessage} from 'react-intl'; +import {useIntl, FormattedMessage, defineMessage} from 'react-intl'; import MenuBarMenu from './menu-bar-menu.jsx'; import {MenuItem, MenuSection} from '../menu/menu.jsx'; import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; -import intlShape from '../../lib/intlShape.js'; import propTypes from '../../lib/prop-types.js'; + +const EditorModes = { + NOW: 'NOW', + MODE_2020: '2020' +}; + const modeMenu = defineMessage({ id: 'gui.aria.modeMenu', defaultMessage: 'Mode menu', @@ -19,13 +24,13 @@ const modeMenu = defineMessage({ const ModeMenu = props => { const { - intl, isRtl, mode2020, modeNow, onSetMode, menuRef } = props; + const intl = useIntl(); const normalRef = React.createRef(); const caturdayRef = React.createRef(); @@ -48,13 +53,12 @@ const ModeMenu = props => { }); return ( -
    { > @@ -90,7 +94,7 @@ const ModeMenu = props => { /> @@ -106,12 +110,11 @@ const ModeMenu = props => { -
    + ); }; ModeMenu.propTypes = { - intl: intlShape.isRequired, menuRef: propTypes.ref.isRequired, onSetMode: PropTypes.func.isRequired, modeNow: PropTypes.bool.isRequired, diff --git a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx index 48efbf0635..280c9558c8 100644 --- a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx @@ -84,13 +84,12 @@ const PreferenceMenu = props => { return ( -
    @@ -105,7 +104,7 @@ const PreferenceMenu = props => { className={styles.expandCaret} src={dropdownCaret} /> -
    + {itemKeys.map((itemKey, index) => ( { canChangeColorMode, canChangeTheme, hasActiveMembership, - intl, isRtl, activeColorMode, onChangeColorMode, activeTheme, onChangeTheme } = props; + const intl = useIntl(); const enabledColorModesMap = useMemo(() => Object.keys(colorModeMap).reduce((acc, colorMode) => { if (enabledColorModes.includes(colorMode)) { @@ -96,11 +95,10 @@ const SettingsMenu = props => { depth: 1 }); - return (
    { > {canChangeLanguage && } @@ -163,12 +160,11 @@ const SettingsMenu = props => { />} -
    ); + ); }; SettingsMenu.propTypes = { menuRef: propTypes.ref.isRequired, - intl: intlShape.isRequired, canChangeLanguage: PropTypes.bool.isRequired, canChangeColorMode: PropTypes.bool.isRequired, canChangeTheme: PropTypes.bool.isRequired, diff --git a/packages/scratch-gui/src/contexts/menu-ref-context.jsx b/packages/scratch-gui/src/contexts/menu-ref-context.jsx index 75be03f24d..8ec6168ce2 100644 --- a/packages/scratch-gui/src/contexts/menu-ref-context.jsx +++ b/packages/scratch-gui/src/contexts/menu-ref-context.jsx @@ -3,10 +3,35 @@ import PropTypes from 'prop-types'; export const MenuRefContext = React.createContext(null); +/** + * This provider manages references to menu components in order to ensure + * sensible behavior for handling menu opening and closing logic + * @param {object} props + * Provider props. + * @param {React.ReactNode} props.children + * Child components that use the logic of the provider. + * @returns {React.ReactNode} + * A MenuRefContext provider exposing: + * - refStack: Array of currently opened menus one after the other + * - openInnerMenu(ref, depth) + * Adds menu at said depth, closing any inner menus that followed previously. + * - closeMenuByRef(ref) + * Closes the specified menu and all menus nested in it. + * - closeInnerMenu() + * Closes only the current innermost menu. + * - closeAllMenus() + * Closes all open menus. + * - isOpenMenu(ref) + * Returns if the given menu is currently open. + * - isInnermostMenu(ref) + * Returns if the given menu is currently the innermost one. + * - outermostMenu + * Returns ref of the outermost open menu. + */ export const MenuRefProvider = ({children}) => { const [refStack, setRefStack] = useState([]); - const cut = useCallback(ref => { + const closeMenuByRef = useCallback(ref => { setRefStack(prev => { const index = prev.indexOf(ref); if (index === -1) return prev; @@ -14,7 +39,7 @@ export const MenuRefProvider = ({children}) => { }); }, []); - const push = useCallback((ref, depth) => { + const openInnerMenu = useCallback((ref, depth) => { setRefStack(prev => { let next = prev; @@ -29,41 +54,40 @@ export const MenuRefProvider = ({children}) => { return [...next, ref]; }); }, []); - - const pop = useCallback(() => { + const closeInnerMenu = useCallback(() => { setRefStack(prev => prev.slice(0, prev.length - 1)); }, []); - const clear = useCallback(() => { + const closeAllMenus = useCallback(() => { setRefStack([]); }, []); - const bottomMenu = useMemo(() => (refStack.length > 0 ? refStack[0] : null), [refStack]); + const outermostMenu = useMemo(() => (refStack.length > 0 ? refStack[0] : null), [refStack]); - const isTopMenu = useCallback(ref => (refStack.length > 0 && + const isInnermostMenu = useCallback(ref => (refStack.length > 0 && refStack[refStack.length - 1] === ref), [refStack]); const isOpenMenu = useCallback(ref => (refStack.includes(ref)), [refStack]); const value = useMemo(() => ({ refStack, - push, - pop, - cut, - clear, - isTopMenu, + openInnerMenu, + closeInnerMenu, + closeMenuByRef, + closeAllMenus, + isInnermostMenu, isOpenMenu, - bottomMenu + outermostMenu }), [ refStack, - push, - pop, - cut, - clear, - isTopMenu, + openInnerMenu, + closeInnerMenu, + closeMenuByRef, + closeAllMenus, + isInnermostMenu, isOpenMenu, - bottomMenu + outermostMenu ]); return ( diff --git a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx index ee59f6055c..a86e286f7f 100644 --- a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx +++ b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx @@ -1,6 +1,16 @@ import {useCallback, useContext, useState, useEffect} from 'react'; import {MenuRefContext} from '../contexts/menu-ref-context'; +const KEY = { + ARROW_UP: 'ArrowUp', + ARROW_DOWN: 'ArrowDown', + ARROW_LEFT: 'ArrowLeft', + ARROW_RIGHT: 'ArrowRight', + ESCAPE: 'Escape', + TAB: 'Tab', + SPACE: ' ' +}; + /** * Provides keyboard navigation and focus management logic for menu components. * @@ -56,17 +66,20 @@ export default function useMenuNavigation ({ const handleOnOpen = useCallback(() => { if (menuContext.isOpenMenu(menuRef)) return; - menuContext.push(menuRef, depth); + menuContext.openInnerMenu(menuRef, depth); setFocusedIndex(defaultIndexOnOpen); }, [menuContext, menuRef, depth]); const handleOnClose = useCallback(() => { setFocusedIndex(-1); - menuContext.cut(menuRef); + menuContext.closeMenuByRef(menuRef); refocusRef(menuRef); }, [menuContext, menuRef, refocusRef]); const handleMove = useCallback(direction => { + // Calculate the next focused menu item index based on the direction. + // Wraps around the list so that moving past the first or last item + // loops to the other end, preventing out-of-bounds errors. const nextIndex = (focusedIndex + direction + itemRefs.length) % itemRefs.length; @@ -75,29 +88,29 @@ export default function useMenuNavigation ({ }, [focusedIndex, itemRefs, refocusRef]); const handleKeyPressOpenMenu = useCallback(e => { - if (e.key === 'ArrowDown') { + if (e.key === KEY.ARROW_DOWN) { e.preventDefault(); handleMove(1); } - if (e.key === 'ArrowUp') { + if (e.key === KEY.ARROW_UP) { e.preventDefault(); handleMove(-1); } - if (e.key === 'ArrowLeft' || e.key === 'Escape') { + if (e.key === KEY.ARROW_LEFT || e.key === KEY.ESCAPE) { e.preventDefault(); handleOnClose(); } }, [handleMove, handleOnClose, menuContext]); const handleKeyPress = useCallback(e => { - if (isExpanded() && depth === 1 && e.key === 'Tab') { + if (isExpanded() && depth === 1 && e.key === KEY.TAB) { handleOnClose(); - menuContext.clear(); + menuContext.closeAllMenus(); } - if (menuContext.isTopMenu(menuRef)) { + if (menuContext.isInnermostMenu(menuRef)) { handleKeyPressOpenMenu(e); - } else if (!isExpanded() && (e.key === ' ' || (e.key === 'ArrowRight' && depth !== 1))) { + } else if (!isExpanded() && (e.key === KEY.SPACE || (e.key === KEY.ARROW_RIGHT && depth !== 1))) { e.preventDefault(); handleOnOpen(); } From 824f352c0fc45d1bbb8dfb5794890a604fc567aa Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 12 Jan 2026 09:36:13 +0200 Subject: [PATCH 29/66] chore: tiny bit more refactoring --- .../scratch-gui/src/components/menu-bar/language-menu.jsx | 4 ++-- packages/scratch-gui/src/components/menu-bar/mode-menu.jsx | 6 +++--- packages/scratch-gui/src/hooks/use-menu-navigation.jsx | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx index e8f8f49ce7..a31752e6aa 100644 --- a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx @@ -32,7 +32,7 @@ const LanguageMenu = props => { const intl = useIntl(); const itemRefs = React.useMemo(() => Object.keys(locales).map(() => React.createRef()), []); - let selectedRef = useRef(null); + const selectedRef = useRef(null); const { isExpanded, @@ -54,7 +54,7 @@ const LanguageMenu = props => { }, [currentLocale, isExpanded, itemRefs]); const setRef = useCallback(component => { - selectedRef = component; + selectedRef.current = component; }, []); const handleMouseOver = useCallback(() => { diff --git a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx index 7586436c83..30e3ac0aa8 100644 --- a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useRef} from 'react'; import styles from './menu-bar.css'; import classNames from 'classnames'; import PropTypes from 'prop-types'; @@ -32,8 +32,8 @@ const ModeMenu = props => { } = props; const intl = useIntl(); - const normalRef = React.createRef(); - const caturdayRef = React.createRef(); + const normalRef = useRef(null); + const caturdayRef = useRef(null); const itemRefs = [ normalRef, diff --git a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx index a86e286f7f..9325cda084 100644 --- a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx +++ b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx @@ -88,6 +88,7 @@ export default function useMenuNavigation ({ }, [focusedIndex, itemRefs, refocusRef]); const handleKeyPressOpenMenu = useCallback(e => { + // Logic for vertical menus, will need to change when implementing for vertical if (e.key === KEY.ARROW_DOWN) { e.preventDefault(); handleMove(1); From 8c76688daf521336af948735d1f5d8da38d2f5ce Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 12 Jan 2026 11:01:43 +0200 Subject: [PATCH 30/66] chore: fix pipeline fail --- .../scratch-gui/src/components/menu-bar/settings-menu.jsx | 4 ++-- packages/scratch-gui/src/containers/language-selector.jsx | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx index 5e0e0549b7..3d55cd7538 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx @@ -80,6 +80,8 @@ const SettingsMenu = props => { const colorRef = useRef(null); const itemRefs = [ ...(canChangeLanguage ? [languageRef] : []), + // TODO: If we do change the condition below for displaying the element, we have + // to change it here also. Perhaps a variable for those conditions might be of use ...(canChangeTheme && availableThemesLength > 1 ? [themeRef] : []), ...(canChangeColorMode ? [colorRef] : []) ]; @@ -184,12 +186,10 @@ const mapStateToProps = state => ({ const mapDispatchToProps = (dispatch, ownProps) => ({ onChangeColorMode: colorMode => { dispatch(setColorMode(colorMode)); - ownProps.onClose(); persistColorMode(colorMode); }, onChangeTheme: theme => { dispatch(setTheme(theme)); - ownProps.onClose(); persistTheme(theme); } }); diff --git a/packages/scratch-gui/src/containers/language-selector.jsx b/packages/scratch-gui/src/containers/language-selector.jsx index 0cb9f08846..b39ace7cd1 100644 --- a/packages/scratch-gui/src/containers/language-selector.jsx +++ b/packages/scratch-gui/src/containers/language-selector.jsx @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; import {selectLocale} from '../reducers/locales'; -import {closeLanguageMenu} from '../reducers/menus'; import LanguageSelectorComponent from '../components/language-selector/language-selector.jsx'; @@ -56,7 +55,6 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => ({ onChangeLanguage: locale => { dispatch(selectLocale(locale)); - dispatch(closeLanguageMenu()); } }); From 58b77bfa4bcf6d80e4c6bc5a98fb68ed60b6c8c9 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 12 Jan 2026 12:50:41 +0200 Subject: [PATCH 31/66] chore: fixing test --- packages/scratch-gui/src/components/gui/gui.jsx | 2 ++ .../scratch-gui/test/unit/components/menu-bar.test.jsx | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/scratch-gui/src/components/gui/gui.jsx b/packages/scratch-gui/src/components/gui/gui.jsx index e69cd10a93..d7832a3c51 100644 --- a/packages/scratch-gui/src/components/gui/gui.jsx +++ b/packages/scratch-gui/src/components/gui/gui.jsx @@ -331,6 +331,8 @@ const GUIComponent = props => { onRequestClose={onRequestCloseBackdropLibrary} /> ) : null} + {/* TODO - in case of moving MenuRefProvider which seems likely, + make sure to move it from tests as well */} {!menuBarHidden && { const store = configureStore()({ @@ -37,7 +38,11 @@ describe('MenuBar Component', () => { }); const getComponent = function (props = {}) { - return ; + return ( + + + + ); }; test('menu bar with no About handler has no About button', () => { From 408f6e2a75c22e9d8aa6665bc761be9c9c852501 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 12 Jan 2026 15:12:38 +0200 Subject: [PATCH 32/66] chore: package-lock.json --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 5950dfc262..bf5e5fff4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41686,4 +41686,4 @@ } } } -} \ No newline at end of file +} From c4c59b13d786bb8b5187533bcf99b18f78f6acd1 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 12 Jan 2026 16:46:27 +0200 Subject: [PATCH 33/66] chore: fixed unit test --- .../src/components/menu-bar/menu-bar.jsx | 18 +++++++++++++----- .../test/unit/components/menu-bar.test.jsx | 8 ++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx index 9b603af793..0e0bb9b0bc 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx @@ -1,7 +1,7 @@ import classNames from 'classnames'; import {connect} from 'react-redux'; import {compose} from 'redux'; -import {defineMessages, FormattedMessage, injectIntl} from 'react-intl'; +import {defineMessages, FormattedMessage, injectIntl, useIntl} from 'react-intl'; import intlShape from '../../lib/intlShape.js'; import PropTypes from 'prop-types'; import bindAll from 'lodash.bindall'; @@ -97,6 +97,11 @@ const ariaMessages = defineMessages({ id: 'gui.menuBar.home', defaultMessage: 'Home', description: 'ARIA text for the home button' + }, + about: { + id: 'gui.menuBar.about', + defaultMessage: 'About', + description: 'ARIA text for the about button' } }); @@ -156,14 +161,17 @@ MenuItemTooltip.propTypes = { isRtl: PropTypes.bool }; -const AboutButton = props => ( - + ); +}; + +AboutMenu.propTypes = { + menuRef: propTypes.ref.isRequired, + isRtl: PropTypes.bool.isRequired, + onClick: PropTypes.oneOfType([ + PropTypes.func, // button mode: call this callback when the About button is clicked + PropTypes.arrayOf( // menu mode: list of items in the About menu + PropTypes.shape({ + title: PropTypes.string, // text for the menu item + onClick: PropTypes.func // call this callback when the menu item is clicked + }) + ) + ]) +}; + +const mapStateToProps = state => ({ + isRtl: state.locales.isRtl +}); + +export default connect(mapStateToProps)(AboutMenu); diff --git a/packages/scratch-gui/src/components/menu-bar/account-menu.jsx b/packages/scratch-gui/src/components/menu-bar/account-menu.jsx new file mode 100644 index 0000000000..219c281935 --- /dev/null +++ b/packages/scratch-gui/src/components/menu-bar/account-menu.jsx @@ -0,0 +1,219 @@ +import styles from './menu-bar.css'; +import classNames from 'classnames'; +import React, {useMemo, useRef} from 'react'; +import useMenuNavigation from '../../hooks/use-menu-navigation'; +import {useIntl, FormattedMessage, defineMessage} from 'react-intl'; +import {connect} from 'react-redux'; +import PropTypes from 'prop-types'; +import propTypes from '../../lib/prop-types'; + +import {AccountMenuOptionsPropTypes} from '../../lib/account-menu-options'; + +import MenuBarMenu from './menu-bar-menu.jsx'; +import {MenuSection} from '../menu/menu.jsx'; +import MenuItemContainer from '../../containers/menu-item.jsx'; +import UserAvatar from './user-avatar.jsx'; +import dropdownCaret from './dropdown-caret.svg'; + +const accountMenu = defineMessage({ + id: 'gui.aria.accountMenu', + defaultMessage: 'Account menu', + description: 'ARIA label for account menu' +}); + +const AccountMenu = props => { + const { + menuRef, + menuOpts, + username, + isRtl, + onLogOut, + avatarBadge + } = props; + + const { + avatarUrl, + myStuffUrl, + profileUrl, + myClassesUrl, + myClassUrl, + accountSettingsUrl, + canLogout + } = menuOpts; + + const intl = useIntl(); + + const profileRef = useRef(null); + const myStuffRef = useRef(null); + const myClassesRef = useRef(null); + const myClassRef = useRef(null); + const accountSettingsRef = useRef(null); + const logoutRef = useRef(null); + + const itemRefs = useMemo(() => [ + ...(profileUrl ? [profileRef] : []), + ...(myStuffUrl ? [myStuffRef] : []), + ...(myClassesUrl ? [myClassesRef] : []), + ...(myClassUrl ? [myClassRef] : []), + ...(accountSettingsUrl ? [accountSettingsRef] : []), + ...(canLogout ? [logoutRef] : []) + ], [profileUrl, myStuffUrl, myClassesUrl, myClassUrl, accountSettingsUrl, canLogout]); + + const { + isExpanded, + handleOnOpen, + handleOnClose, + handleKeyPress, + handleKeyPressOpenMenu + } = useMenuNavigation({ + menuRef, + itemRefs, + depth: 1 + }); + + return ( + + + + {profileUrl ? ( + + + + ) : null} + + {myStuffUrl ? ( + + + + ) : null} + + {myClassesUrl ? ( + + + + ) : null} + + {myClassUrl ? ( + + + + ) : null} + + {accountSettingsUrl ? ( + + + + ) : null} + + {canLogout ? ( + + + + + + ) : null} + + + ); +}; + +AccountMenu.propTypes = { + menuRef: propTypes.ref.isRequired, + menuOpts: AccountMenuOptionsPropTypes, + isRtl: PropTypes.bool, + username: PropTypes.string, + onLogOut: PropTypes.func, + avatarBadge: PropTypes.number +}; + +const mapStateToProps = state => ({ + isRtl: state.locales.isRtl +}); + +export default connect( + mapStateToProps +)(AccountMenu); diff --git a/packages/scratch-gui/src/components/menu-bar/account-nav.css b/packages/scratch-gui/src/components/menu-bar/account-nav.css deleted file mode 100644 index e868d03447..0000000000 --- a/packages/scratch-gui/src/components/menu-bar/account-nav.css +++ /dev/null @@ -1,53 +0,0 @@ -@import "../../css/colors.css"; -@import "../../css/units.css"; - -.user-info { - display: inline-flex; - flex-wrap: nowrap; - justify-content: center; - align-items: center; - align-content: center; - padding: 0 0.95rem; - max-width: 260px; - height: $menu-bar-height; - overflow: hidden; - text-decoration: none; - text-overflow: ellipsis; - white-space: nowrap; - color: $ui-white; - font-size: .75rem; - font-weight: normal; -} - -[dir="ltr"] .user-info .avatar { - margin-right: calc($space * .8125); -} - -[dir="rtl"] .user-info .avatar { - margin-left: calc($space * .8125); -} - -.user-info .avatar { - width: 2rem; - height: 2rem; -} - -[dir="ltr"] .user-info .dropdown-caret-position { - margin-left: calc($space * .8125); -} - -[dir="rtl"] .user-info .dropdown-caret-position { - margin-right: calc($space * .8125); -} - -.user-info .dropdown-caret-position { - display: inline-block; - padding-bottom: .0625rem; - vertical-align: middle; -} - -.user-info .profile-name { - font-size: .75rem; - line-height: .9375rem; - font-weight: bold; -} diff --git a/packages/scratch-gui/src/components/menu-bar/account-nav.jsx b/packages/scratch-gui/src/components/menu-bar/account-nav.jsx deleted file mode 100644 index eb79d5d4a3..0000000000 --- a/packages/scratch-gui/src/components/menu-bar/account-nav.jsx +++ /dev/null @@ -1,158 +0,0 @@ -/* -NOTE: this file only temporarily resides in scratch-gui. -Nearly identical code appears in scratch-www, and the two should -eventually be consolidated. -*/ - -import classNames from 'classnames'; -import {FormattedMessage} from 'react-intl'; -import PropTypes from 'prop-types'; -import React from 'react'; - -import MenuBarMenu from './menu-bar-menu.jsx'; -import {MenuSection} from '../menu/menu.jsx'; -import MenuItemContainer from '../../containers/menu-item.jsx'; -import UserAvatar from './user-avatar.jsx'; -import dropdownCaret from './dropdown-caret.svg'; - -import styles from './account-nav.css'; - -const AccountNavComponent = ({ - className, - isOpen, - isRtl, - menuBarMenuClassName, - onClick, - onClose, - onLogOut, - profileUrl, - myStuffUrl, - avatarUrl, - myClassesUrl, - myClassUrl, - accountSettingsUrl, - username, - avatarBadge -}) => ( - -
    - {avatarUrl ? ( - - ) : null} - - {username} - -
    - -
    -
    - - {profileUrl ? ( - - - - ) : null} - - {myStuffUrl ? ( - - - - ) : null} - - {myClassesUrl ? ( - - - - ) : null} - - {myClassUrl ? ( - - - - ) : null} - - {accountSettingsUrl ? ( - - - - ) : null} - - {onLogOut ? ( - - - - - - ) : null} - -
    -); - -AccountNavComponent.propTypes = { - className: PropTypes.string, - - isOpen: PropTypes.bool, - isRtl: PropTypes.bool, - - menuBarMenuClassName: PropTypes.string, - - onClick: PropTypes.func, - onClose: PropTypes.func, - onLogOut: PropTypes.func, - - username: PropTypes.string, - avatarBadge: PropTypes.number, - - avatarUrl: PropTypes.string, - myStuffUrl: PropTypes.string, - profileUrl: PropTypes.string, - myClassesUrl: PropTypes.string, - myClassUrl: PropTypes.string, - accountSettingsUrl: PropTypes.string -}; - -export default AccountNavComponent; diff --git a/packages/scratch-gui/src/components/menu-bar/author-info.jsx b/packages/scratch-gui/src/components/menu-bar/author-info.jsx index 4c769297be..34de79b243 100644 --- a/packages/scratch-gui/src/components/menu-bar/author-info.jsx +++ b/packages/scratch-gui/src/components/menu-bar/author-info.jsx @@ -1,11 +1,17 @@ import PropTypes from 'prop-types'; import React from 'react'; import classNames from 'classnames'; -import {FormattedMessage} from 'react-intl'; +import {FormattedMessage, defineMessage, useIntl} from 'react-intl'; import UserAvatar from './user-avatar.jsx'; import styles from './author-info.css'; +const authorInfo = defineMessage({ + id: 'gui.aria.authorInfo', + defaultMessage: 'Project "{projectTitle}" by {username}', + description: 'ARIA label for author info' +}); + const AuthorInfo = ({ className, imageUrl, @@ -14,8 +20,10 @@ const AuthorInfo = ({ userId, username, avatarBadge -}) => ( -
    { + const intl = useIntl(); + + return (
    -
    +
    {projectTitle} @@ -48,8 +64,8 @@ const AuthorInfo = ({
    -
    -); +
    ); +}; AuthorInfo.propTypes = { className: PropTypes.string, diff --git a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx index 1f7e7f5d96..df99373389 100644 --- a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx @@ -54,7 +54,6 @@ const EditMenu = props => { onClick={handleOnOpen} aria-label={intl.formatMessage(editMenu)} aria-expanded={isExpanded()} - tabIndex={0} onKeyDown={handleKeyPress} ref={menuRef} > diff --git a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx index 094be3f874..45a81f8095 100644 --- a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx @@ -102,7 +102,6 @@ const FileMenu = props => { onClick={handleOnOpen} aria-label={intl.formatMessage(fileMenu)} aria-expanded={isExpanded()} - tabIndex={0} ref={menuRef} onKeyDown={handleKeyPress} > diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.css b/packages/scratch-gui/src/components/menu-bar/menu-bar.css index 7aa3610348..6bf6cb37dd 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.css +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.css @@ -242,3 +242,54 @@ margin-right: 0; } } + +.user-info { + display: inline-flex; + flex-wrap: nowrap; + justify-content: center; + align-items: center; + align-content: center; + padding: 0 0.95rem; + max-width: 260px; + height: $menu-bar-height; + overflow: hidden; + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; + color: $ui-white; + font-size: .75rem; + font-weight: normal; +} + +[dir="ltr"] .user-info .avatar { + margin-right: calc($space * .8125); +} + +[dir="rtl"] .user-info .avatar { + margin-left: calc($space * .8125); +} + +.user-info .avatar { + width: 2rem; + height: 2rem; +} + +[dir="ltr"] .user-info .dropdown-caret-position { + margin-left: calc($space * .8125); +} + +[dir="rtl"] .user-info .dropdown-caret-position { + margin-right: calc($space * .8125); +} + +.user-info .dropdown-caret-position { + display: inline-block; + padding-bottom: .0625rem; + vertical-align: middle; +} + +.user-info .profile-name { + font-size: .75rem; + line-height: .9375rem; + font-weight: bold; +} diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx index 0e0bb9b0bc..410c91f71e 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx @@ -1,7 +1,7 @@ import classNames from 'classnames'; import {connect} from 'react-redux'; import {compose} from 'redux'; -import {defineMessages, FormattedMessage, injectIntl, useIntl} from 'react-intl'; +import {defineMessages, FormattedMessage, injectIntl} from 'react-intl'; import intlShape from '../../lib/intlShape.js'; import PropTypes from 'prop-types'; import bindAll from 'lodash.bindall'; @@ -18,17 +18,15 @@ import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx'; import Divider from '../divider/divider.jsx'; import SaveStatus from './save-status.jsx'; import ProjectWatcher from '../../containers/project-watcher.jsx'; -import MenuBarMenu from './menu-bar-menu.jsx'; -import {MenuItem} from '../menu/menu.jsx'; import ProjectTitleInput from './project-title-input.jsx'; import AuthorInfo from './author-info.jsx'; -import AccountNav from '../../components/menu-bar/account-nav.jsx'; import LoginDropdown from './login-dropdown.jsx'; import MenuBarHOC from '../../containers/menu-bar-hoc.jsx'; import SettingsMenu from './settings-menu.jsx'; import FileMenu from './file-menu.jsx'; import EditMenu from './edit-menu.jsx'; import ModeMenu from './mode-menu.jsx'; +import AboutMenu from './about-menu.jsx'; import {openTipsLibrary, openDebugModal} from '../../reducers/modals'; import {setPlayer} from '../../reducers/mode'; @@ -47,12 +45,6 @@ import { requestNewProject } from '../../reducers/project-state'; import { - openAboutMenu, - closeAboutMenu, - aboutMenuOpen, - openAccountMenu, - closeAccountMenu, - accountMenuOpen, openLoginMenu, closeLoginMenu, loginMenuOpen @@ -68,7 +60,6 @@ import mystuffIcon from './icon--mystuff.png'; import profileIcon from './icon--profile.png'; import remixIcon from './icon--remix.svg'; import dropdownCaret from './dropdown-caret.svg'; -import aboutIcon from './icon--about.svg'; import debugIcon from '../debug-modal/icons/icon--debug.svg'; import scratchLogo from './scratch-logo.svg'; @@ -81,6 +72,7 @@ import oldtimeyLogo from './oldtimey-logo.svg'; import sharedMessages from '../../lib/shared-messages'; import {AccountMenuOptionsPropTypes} from '../../lib/account-menu-options'; +import AccountMenu from './account-menu.jsx'; const ariaMessages = defineMessages({ tutorials: { @@ -97,17 +89,12 @@ const ariaMessages = defineMessages({ id: 'gui.menuBar.home', defaultMessage: 'Home', description: 'ARIA text for the home button' - }, - about: { - id: 'gui.menuBar.about', - defaultMessage: 'About', - description: 'ARIA text for the about button' } }); const getScratchLogo = platform => (platform === PLATFORM.ANDROID ? scratchLogoAndroid : scratchLogo); -const MenuBarItemTooltip = ({ +export const MenuBarItemTooltip = ({ children, className, enable, @@ -133,7 +120,6 @@ const MenuBarItemTooltip = ({ ); }; - MenuBarItemTooltip.propTypes = { children: PropTypes.node, className: PropTypes.string, @@ -161,22 +147,6 @@ MenuItemTooltip.propTypes = { isRtl: PropTypes.bool }; -const AboutButton = props => { - const intl = useIntl(); - - return ( ) : null} {menuOpts.canLogin ? ( -
    -
    + ) : null} ) @@ -725,14 +629,19 @@ class MenuBar extends React.Component { )}
    - {aboutButton} + {this.props.onClickAbout && ( + + )} ); } } MenuBar.propTypes = { - aboutMenuOpen: PropTypes.bool, accountMenuOpen: PropTypes.bool, ariaLabel: PropTypes.string, ariaRole: PropTypes.string, @@ -779,9 +688,6 @@ MenuBar.propTypes = { }) ) ]), - onClickAccount: PropTypes.func, - onClickEdit: PropTypes.func, - onClickFile: PropTypes.func, onClickLogin: PropTypes.func, onClickLogo: PropTypes.func, onClickMode: PropTypes.func, @@ -789,16 +695,12 @@ MenuBar.propTypes = { onClickRemix: PropTypes.func, onClickSave: PropTypes.func, onClickSaveAsCopy: PropTypes.func, - onClickSettings: PropTypes.func, onLogOut: PropTypes.func, onOpenRegistration: PropTypes.func, onOpenTipLibrary: PropTypes.func, onOpenDebugModal: PropTypes.func, onProjectTelemetryEvent: PropTypes.func, - onRequestCloseAbout: PropTypes.func, - onRequestCloseAccount: PropTypes.func, onRequestCloseLogin: PropTypes.func, - onRequestOpenAbout: PropTypes.func, onSeeCommunity: PropTypes.func, onSetTimeTravelMode: PropTypes.func, onShare: PropTypes.func, @@ -830,8 +732,6 @@ const mapStateToProps = (state, ownProps) => { const sessionExists = state.session && typeof state.session.session !== 'undefined'; return { - aboutMenuOpen: aboutMenuOpen(state), - accountMenuOpen: accountMenuOpen(state), currentLocale: state.locales.locale, isRtl: state.locales.isRtl, isUpdating: getIsUpdating(loadingState), @@ -876,13 +776,9 @@ const mapDispatchToProps = (dispatch, ownProps) => ({ autoUpdateProject: () => dispatch(autoUpdateProject()), onOpenTipLibrary: () => dispatch(openTipsLibrary()), onOpenDebugModal: () => dispatch(openDebugModal()), - onClickAccount: () => dispatch(openAccountMenu()), - onRequestCloseAccount: () => dispatch(closeAccountMenu()), onClickNew: needSave => dispatch(requestNewProject(needSave)), onClickLogin: ownProps.onClickLogin ?? (() => dispatch(openLoginMenu())), onRequestCloseLogin: () => dispatch(closeLoginMenu()), - onRequestOpenAbout: () => dispatch(openAboutMenu()), - onRequestCloseAbout: () => dispatch(closeAboutMenu()), onSeeCommunity: ownProps.onSeeCommunity ?? (() => dispatch(setPlayer(true))), onSetTimeTravelMode: mode => dispatch(setTimeTravel(mode)) }); diff --git a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx index 30e3ac0aa8..a64b2e1f5e 100644 --- a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx @@ -61,7 +61,6 @@ const ModeMenu = props => { ref={menuRef} aria-label={intl.formatMessage(modeMenu)} aria-expanded={isExpanded()} - tabIndex={0} onKeyDown={handleKeyPress} >
    diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx index 3d55cd7538..bc08588dad 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx @@ -58,6 +58,7 @@ const SettingsMenu = props => { activeTheme, onChangeTheme } = props; + const intl = useIntl(); const enabledColorModesMap = useMemo(() => Object.keys(colorModeMap).reduce((acc, colorMode) => { @@ -102,7 +103,6 @@ const SettingsMenu = props => { [menuBarStyles.active]: isExpanded() })} aria-expanded={isExpanded()} - tabIndex={0} aria-label={intl.formatMessage(ariaMessages.settingsMenu)} onClick={handleOnOpen} onKeyDown={handleKeyPress} @@ -183,7 +183,7 @@ const mapStateToProps = state => ({ activeTheme: state.scratchGui.settings.theme }); -const mapDispatchToProps = (dispatch, ownProps) => ({ +const mapDispatchToProps = dispatch => ({ onChangeColorMode: colorMode => { dispatch(setColorMode(colorMode)); persistColorMode(colorMode); diff --git a/packages/scratch-gui/src/containers/menu-item.jsx b/packages/scratch-gui/src/containers/menu-item.jsx index 8755c19f73..29c17bf594 100644 --- a/packages/scratch-gui/src/containers/menu-item.jsx +++ b/packages/scratch-gui/src/containers/menu-item.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import {MenuItem as MenuItemComponent} from '../components/menu/menu.jsx'; +import propTypes from '../lib/prop-types.js'; class MenuItem extends React.Component { constructor (props) { @@ -18,13 +19,17 @@ class MenuItem extends React.Component { const { children, className, - onClick + onClick, + itemRef, + onParentKeyPress } = this.props; const clickAction = onClick ? onClick : this.navigateToHref; return ( {children} @@ -37,7 +42,9 @@ MenuItem.propTypes = { className: PropTypes.string, // can take an onClick prop, or take an href and build an onClick handler href: PropTypes.string, - onClick: PropTypes.func + onClick: PropTypes.func, + itemRef: propTypes.ref, + onParentKeyPress: PropTypes.func }; export default MenuItem; diff --git a/packages/scratch-gui/src/reducers/menus.js b/packages/scratch-gui/src/reducers/menus.js index 2d75a9fdfc..ff390817dc 100644 --- a/packages/scratch-gui/src/reducers/menus.js +++ b/packages/scratch-gui/src/reducers/menus.js @@ -1,8 +1,6 @@ const OPEN_MENU = 'scratch-gui/menus/OPEN_MENU'; const CLOSE_MENU = 'scratch-gui/menus/CLOSE_MENU'; -const MENU_ABOUT = 'aboutMenu'; -const MENU_ACCOUNT = 'accountMenu'; const MENU_LOGIN = 'loginMenu'; class Menu { @@ -42,13 +40,9 @@ class Menu { // Structure of nested menus, used for collapsing submenus logic. const rootMenu = new Menu('root') - .addChild(new Menu(MENU_LOGIN)) - .addChild(new Menu(MENU_ACCOUNT)) - .addChild(new Menu(MENU_ABOUT)); + .addChild(new Menu(MENU_LOGIN)); const initialState = { - [MENU_ABOUT]: false, - [MENU_ACCOUNT]: false, [MENU_LOGIN]: false }; @@ -89,14 +83,6 @@ const closeMenu = menu => ({ menu: menu }); -const openAboutMenu = () => openMenu(MENU_ABOUT); -const closeAboutMenu = () => closeMenu(MENU_ABOUT); -const aboutMenuOpen = state => state.scratchGui.menus[MENU_ABOUT]; - -const openAccountMenu = () => openMenu(MENU_ACCOUNT); -const closeAccountMenu = () => closeMenu(MENU_ACCOUNT); -const accountMenuOpen = state => state.scratchGui.menus[MENU_ACCOUNT]; - const openLoginMenu = () => openMenu(MENU_LOGIN); const closeLoginMenu = () => closeMenu(MENU_LOGIN); const loginMenuOpen = state => state.scratchGui.menus[MENU_LOGIN]; @@ -104,12 +90,6 @@ const loginMenuOpen = state => state.scratchGui.menus[MENU_LOGIN]; export { reducer as default, initialState as menuInitialState, - openAboutMenu, - closeAboutMenu, - aboutMenuOpen, - openAccountMenu, - closeAccountMenu, - accountMenuOpen, openLoginMenu, closeLoginMenu, loginMenuOpen From 7599d054c98a044e5f57c9e11e12212ad619852d Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Thu, 15 Jan 2026 15:11:26 +0200 Subject: [PATCH 35/66] chore: refactored code, addressed comments --- .../src/components/menu-bar/about-menu.jsx | 6 +++- .../src/components/menu-bar/account-menu.jsx | 18 +++++------- .../src/components/menu-bar/edit-menu.jsx | 13 ++++----- .../src/components/menu-bar/file-menu.jsx | 29 +++++++++---------- .../src/components/menu-bar/language-menu.jsx | 17 +++++------ .../src/components/menu-bar/mode-menu.jsx | 15 +++++----- .../components/menu-bar/preference-menu.jsx | 22 +++++++------- .../src/components/menu-bar/settings-menu.jsx | 26 ++++++++--------- .../src/hooks/use-menu-navigation.jsx | 8 ++--- 9 files changed, 74 insertions(+), 80 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/about-menu.jsx b/packages/scratch-gui/src/components/menu-bar/about-menu.jsx index dcffe58b8a..e6a06abebf 100644 --- a/packages/scratch-gui/src/components/menu-bar/about-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/about-menu.jsx @@ -35,7 +35,11 @@ AboutButton.propTypes = { onClick: PropTypes.func.isRequired }; -const AboutMenu = ({menuRef, onClick, isRtl}) => { +const AboutMenu = ({ + menuRef, + onClick, + isRtl +}) => { const intl = useIntl(); const itemRefs = onClick.map(() => useRef(null)); diff --git a/packages/scratch-gui/src/components/menu-bar/account-menu.jsx b/packages/scratch-gui/src/components/menu-bar/account-menu.jsx index 219c281935..60c07faca4 100644 --- a/packages/scratch-gui/src/components/menu-bar/account-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/account-menu.jsx @@ -21,16 +21,14 @@ const accountMenu = defineMessage({ description: 'ARIA label for account menu' }); -const AccountMenu = props => { - const { - menuRef, - menuOpts, - username, - isRtl, - onLogOut, - avatarBadge - } = props; - +const AccountMenu = ({ + menuRef, + menuOpts, + username, + isRtl, + onLogOut, + avatarBadge +}) => { const { avatarUrl, myStuffUrl, diff --git a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx index df99373389..fb59f933fd 100644 --- a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx @@ -20,13 +20,12 @@ const editMenu = defineMessage({ description: 'ARIA label for edit menu' }); -const EditMenu = props => { - const { - menuRef, - isRtl, - onRestoreOption, - restoreOptionMessage - } = props; +const EditMenu = ({ + menuRef, + isRtl, + onRestoreOption, + restoreOptionMessage +}) => { const intl = useIntl(); const restoreRef = useRef(null); diff --git a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx index 45a81f8095..2da88d807e 100644 --- a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx @@ -27,21 +27,20 @@ const fileMenu = defineMessage({ description: 'ARIA label for file menu' }); -const FileMenu = props => { - const { - isRtl, - menuRef, - canSave, - canCreateCopy, - canRemix, - onClickNew, - onClickSave, - onClickSaveAsCopy, - onClickRemix, - onStartSelectingFileUpload, - getSaveToComputerHandler, - remixMessage - } = props; +const FileMenu = ({ + isRtl, + menuRef, + canSave, + canCreateCopy, + canRemix, + onClickNew, + onClickSave, + onClickSaveAsCopy, + onClickRemix, + onStartSelectingFileUpload, + getSaveToComputerHandler, + remixMessage +}) => { const intl = useIntl(); const newProjectRef = useRef(null); diff --git a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx index a31752e6aa..0f5aa49da1 100644 --- a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx @@ -22,13 +22,12 @@ const languageMenu = defineMessage({ description: 'ARIA label for language menu' }); -const LanguageMenu = props => { - const { - currentLocale, - menuRef, - isRtl, - onChangeLanguage - } = props; +const LanguageMenu = ({ + currentLocale, + menuRef, + isRtl, + onChangeLanguage +}) => { const intl = useIntl(); const itemRefs = React.useMemo(() => Object.keys(locales).map(() => React.createRef()), []); @@ -59,8 +58,8 @@ const LanguageMenu = props => { const handleMouseOver = useCallback(() => { // If we are using hover rather than clicks for submenus, scroll the selected option into view - if (isExpanded() && selectedRef) { - selectedRef.scrollIntoView({block: 'center'}); + if (isExpanded() && selectedRef.current) { + selectedRef.current.scrollIntoView({block: 'center'}); } }, [isExpanded]); diff --git a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx index a64b2e1f5e..f5937e9e6b 100644 --- a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx @@ -22,14 +22,13 @@ const modeMenu = defineMessage({ description: 'ARIA label for mode menu' }); -const ModeMenu = props => { - const { - isRtl, - mode2020, - modeNow, - onSetMode, - menuRef - } = props; +const ModeMenu = ({ + isRtl, + mode2020, + modeNow, + onSetMode, + menuRef +}) => { const intl = useIntl(); const normalRef = useRef(null); diff --git a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx index 280c9558c8..6306506981 100644 --- a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx @@ -54,18 +54,16 @@ PreferenceItem.propTypes = { itemRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}) }; -const PreferenceMenu = props => { - const { - itemsMap, - onChange, - defaultMenuIconSrc, - submenuLabel, - selectedItemKey, - isRtl, - menuRef, - ariaLabel - } = props; - +const PreferenceMenu = ({ + itemsMap, + onChange, + defaultMenuIconSrc, + submenuLabel, + selectedItemKey, + isRtl, + menuRef, + ariaLabel +}) => { const itemRefs = Object.keys(itemsMap).map(() => React.createRef()); const itemKeys = Object.keys(itemsMap); diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx index bc08588dad..6eaca9870d 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx @@ -45,20 +45,18 @@ const ariaMessages = defineMessages({ const enabledColorModes = [DEFAULT_MODE, HIGH_CONTRAST_MODE]; -const SettingsMenu = props => { - const { - menuRef, - canChangeLanguage, - canChangeColorMode, - canChangeTheme, - hasActiveMembership, - isRtl, - activeColorMode, - onChangeColorMode, - activeTheme, - onChangeTheme - } = props; - +const SettingsMenu = ({ + menuRef, + canChangeLanguage, + canChangeColorMode, + canChangeTheme, + hasActiveMembership, + isRtl, + activeColorMode, + onChangeColorMode, + activeTheme, + onChangeTheme +}) => { const intl = useIntl(); const enabledColorModesMap = useMemo(() => Object.keys(colorModeMap).reduce((acc, colorMode) => { diff --git a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx index 9325cda084..c163fb80c2 100644 --- a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx +++ b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx @@ -56,7 +56,7 @@ export default function useMenuNavigation ({ if (focusedIndex >= 0) { refocusRef(itemRefs[focusedIndex]); } - }, [focusedIndex]); + }, [focusedIndex, refocusRef]); const isExpanded = useCallback( () => menuContext.isOpenMenu(menuRef), @@ -68,7 +68,7 @@ export default function useMenuNavigation ({ menuContext.openInnerMenu(menuRef, depth); setFocusedIndex(defaultIndexOnOpen); - }, [menuContext, menuRef, depth]); + }, [menuContext, menuRef, depth, defaultIndexOnOpen]); const handleOnClose = useCallback(() => { setFocusedIndex(-1); @@ -85,7 +85,7 @@ export default function useMenuNavigation ({ itemRefs.length; setFocusedIndex(nextIndex); - }, [focusedIndex, itemRefs, refocusRef]); + }, [focusedIndex, itemRefs]); const handleKeyPressOpenMenu = useCallback(e => { // Logic for vertical menus, will need to change when implementing for vertical @@ -101,7 +101,7 @@ export default function useMenuNavigation ({ e.preventDefault(); handleOnClose(); } - }, [handleMove, handleOnClose, menuContext]); + }, [handleMove, handleOnClose]); const handleKeyPress = useCallback(e => { if (isExpanded() && depth === 1 && e.key === KEY.TAB) { From 7618f419a5efcb2dc0339bcbc907cb8c582c8e4a Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Thu, 15 Jan 2026 16:09:01 +0200 Subject: [PATCH 36/66] chore: changed accessibility messages and updated jsdoc --- .../scratch-gui/src/components/gui/gui.jsx | 22 +++++++++---------- .../src/components/menu-bar/about-menu.jsx | 2 +- .../src/components/menu-bar/account-menu.jsx | 2 +- .../src/components/menu-bar/author-info.jsx | 2 +- .../src/components/menu-bar/edit-menu.jsx | 2 +- .../src/components/menu-bar/file-menu.jsx | 2 +- .../src/components/menu-bar/language-menu.jsx | 2 +- .../src/components/menu-bar/menu-bar.jsx | 6 ++--- .../src/components/menu-bar/mode-menu.jsx | 2 +- .../src/components/menu-bar/settings-menu.jsx | 6 ++--- .../src/hooks/use-menu-navigation.jsx | 15 +++++++++++++ 11 files changed, 39 insertions(+), 24 deletions(-) diff --git a/packages/scratch-gui/src/components/gui/gui.jsx b/packages/scratch-gui/src/components/gui/gui.jsx index 5a7cfc7005..5539522b26 100644 --- a/packages/scratch-gui/src/components/gui/gui.jsx +++ b/packages/scratch-gui/src/components/gui/gui.jsx @@ -52,57 +52,57 @@ const ariaMessages = defineMessages({ menuBar: { id: 'gui.aria.menuBar', defaultMessage: 'Menu topbar', - description: 'ARIA label for the top menu bar' + description: 'accessibility label for the top menu bar' }, editor: { id: 'gui.aria.editor', defaultMessage: 'Editor', - description: 'ARIA label for the main editor area' + description: 'accessibility label for the main editor area' }, tabList: { id: 'gui.aria.tabList', defaultMessage: 'Tab list', - description: 'ARIA label for the editor tab list' + description: 'accessibility label for the editor tab list' }, codePanel: { id: 'gui.aria.codePanel', defaultMessage: 'Code editor panel', - description: 'ARIA label for the code editor panel' + description: 'accessibility label for the code editor panel' }, costumesPanel: { id: 'gui.aria.costumesPanel', defaultMessage: 'Costumes editor panel', - description: 'ARIA label for the costumes editor panel' + description: 'accessibility label for the costumes editor panel' }, backdropsPanel: { id: 'gui.aria.backdropsPanel', defaultMessage: 'Backdrops editor panel', - description: 'ARIA label for the backdrops editor panel' + description: 'accessibility label for the backdrops editor panel' }, soundsPanel: { id: 'gui.aria.soundsPanel', defaultMessage: 'Sounds editor panel', - description: 'ARIA label for the sounds editor panel' + description: 'accessibility label for the sounds editor panel' }, backpack: { id: 'gui.aria.backpack', defaultMessage: 'Backpack', - description: 'ARIA label for the backpack' + description: 'accessibility label for the backpack' }, stageAndTarget: { id: 'gui.aria.stageAndTarget', defaultMessage: 'Stage and target', - description: 'ARIA label for stage and target area' + description: 'accessibility label for stage and target area' }, stage: { id: 'gui.aria.stage', defaultMessage: 'Stage', - description: 'ARIA label for the stage' + description: 'accessibility label for the stage' }, targetPane: { id: 'gui.aria.targetPane', defaultMessage: 'Target pane', - description: 'ARIA label for the target pane' + description: 'accessibility label for the target pane' } }); diff --git a/packages/scratch-gui/src/components/menu-bar/about-menu.jsx b/packages/scratch-gui/src/components/menu-bar/about-menu.jsx index e6a06abebf..728cc2f446 100644 --- a/packages/scratch-gui/src/components/menu-bar/about-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/about-menu.jsx @@ -16,7 +16,7 @@ import propTypes from '../../lib/prop-types.js'; const aboutMenuMessage = defineMessage({ id: 'gui.aria.aboutMenu', defaultMessage: 'About menu', - description: 'ARIA label for About menu' + description: 'accessibility label for About menu' }); const AboutButton = props => { diff --git a/packages/scratch-gui/src/components/menu-bar/account-menu.jsx b/packages/scratch-gui/src/components/menu-bar/account-menu.jsx index 60c07faca4..ca479b61e4 100644 --- a/packages/scratch-gui/src/components/menu-bar/account-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/account-menu.jsx @@ -18,7 +18,7 @@ import dropdownCaret from './dropdown-caret.svg'; const accountMenu = defineMessage({ id: 'gui.aria.accountMenu', defaultMessage: 'Account menu', - description: 'ARIA label for account menu' + description: 'accessibility label for account menu' }); const AccountMenu = ({ diff --git a/packages/scratch-gui/src/components/menu-bar/author-info.jsx b/packages/scratch-gui/src/components/menu-bar/author-info.jsx index 34de79b243..b763cfb343 100644 --- a/packages/scratch-gui/src/components/menu-bar/author-info.jsx +++ b/packages/scratch-gui/src/components/menu-bar/author-info.jsx @@ -9,7 +9,7 @@ import styles from './author-info.css'; const authorInfo = defineMessage({ id: 'gui.aria.authorInfo', defaultMessage: 'Project "{projectTitle}" by {username}', - description: 'ARIA label for author info' + description: 'accessibility label for author info' }); const AuthorInfo = ({ diff --git a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx index fb59f933fd..45388110f0 100644 --- a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx @@ -17,7 +17,7 @@ import propTypes from '../../lib/prop-types.js'; const editMenu = defineMessage({ id: 'gui.aria.editMenu', defaultMessage: 'Edit menu', - description: 'ARIA label for edit menu' + description: 'accessibility label for edit menu' }); const EditMenu = ({ diff --git a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx index 2da88d807e..0e9ed23325 100644 --- a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx @@ -24,7 +24,7 @@ import { const fileMenu = defineMessage({ id: 'gui.aria.fileMenu', defaultMessage: 'File menu', - description: 'ARIA label for file menu' + description: 'accessibility label for file menu' }); const FileMenu = ({ diff --git a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx index 0f5aa49da1..cc5201ef10 100644 --- a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx @@ -19,7 +19,7 @@ import propTypes from '../../lib/prop-types.js'; const languageMenu = defineMessage({ id: 'gui.aria.languageMenu', defaultMessage: 'Language menu', - description: 'ARIA label for language menu' + description: 'accessibility label for language menu' }); const LanguageMenu = ({ diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx index 410c91f71e..cb7d24e534 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx @@ -78,17 +78,17 @@ const ariaMessages = defineMessages({ tutorials: { id: 'gui.menuBar.tutorialsLibrary', defaultMessage: 'Tutorials', - description: 'ARIA text for the tutorials button' + description: 'accessibility text for the tutorials button' }, debug: { id: 'gui.menuBar.debug', defaultMessage: 'Debug', - description: 'ARIA text for the debug button' + description: 'accessibility text for the debug button' }, home: { id: 'gui.menuBar.home', defaultMessage: 'Home', - description: 'ARIA text for the home button' + description: 'accessibility text for the home button' } }); diff --git a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx index f5937e9e6b..f0bf835460 100644 --- a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx @@ -19,7 +19,7 @@ const EditorModes = { const modeMenu = defineMessage({ id: 'gui.aria.modeMenu', defaultMessage: 'Mode menu', - description: 'ARIA label for mode menu' + description: 'accessibility label for mode menu' }); const ModeMenu = ({ diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx index 6eaca9870d..eca948ed30 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx @@ -29,17 +29,17 @@ const ariaMessages = defineMessages({ settingsMenu: { id: 'gui.aria.settingsMenu', defaultMessage: 'Settings menu', - description: 'ARIA label for settings menu' + description: 'accessibility label for settings menu' }, themeMenu: { id: 'gui.aria.themeMenu', defaultMessage: 'Theme menu', - description: 'ARIA label for theme menu' + description: 'accessibility label for theme menu' }, colorMenu: { id: 'gui.aria.colorMenu', defaultMessage: 'Color menu', - description: 'ARIA label for color menu' + description: 'accessibility label for color menu' } }); diff --git a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx index c163fb80c2..8af5b220d8 100644 --- a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx +++ b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx @@ -19,6 +19,21 @@ const KEY = { * - moving focus between menu items with arrow keys * - handling Escape, Enter, and Tab behavior * - coordinating open menus via MenuRefContext + * STEPS TO USE IT: + * ______________________________________________________________________________________ + * 1. Define itemRefs as an array of refs for the submenu items, then pass + * {itemRefs, menuRef (ref for component itself) and depth (starting from 1 for )} + * 2. In the top component (button/div/...) pass + * - onClick={handleOnOpen} + * - ref={menuRef} + * - onKeyDown={handleKeyPress} + * - tabIndex={0} if it's the core menu accessible via tab or tabIndex={1} if it's a submenu + * - aria-expanded={isExpanded()} (also everywhere else where you require isExpanded() logic, + * such as the css responsible for conditionally displaying the dropdown) + * 3. In the submenu items pass: + * - itemRef={itemRefs[index]} + * - onParentKeyPress={handleKeyPressOpenMenu} + * ______________________________________________________________________________________ * @param {object} params * Parameters object * @param {{ current: HTMLElement | null }} params.menuRef From a0985eaa04d105bd384a48711cd004a78b30028d Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Fri, 16 Jan 2026 10:15:54 +0200 Subject: [PATCH 37/66] chore: more code refactoring and comment addressing --- .../scratch-gui/src/components/menu-bar/edit-menu.jsx | 4 ++-- .../scratch-gui/src/components/menu-bar/menu-bar.jsx | 4 +--- .../src/components/menu-bar/settings-menu.css | 4 ++-- .../scratch-gui/src/hooks/use-menu-navigation.jsx | 11 +---------- packages/scratch-gui/src/lib/navigation-keys.js | 10 ++++++++++ 5 files changed, 16 insertions(+), 17 deletions(-) create mode 100644 packages/scratch-gui/src/lib/navigation-keys.js diff --git a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx index 45388110f0..995ed3a55d 100644 --- a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx @@ -14,7 +14,7 @@ import DeletionRestorer from '../../containers/deletion-restorer.jsx'; import TurboMode from '../../containers/turbo-mode.jsx'; import propTypes from '../../lib/prop-types.js'; -const editMenu = defineMessage({ +const editMenuAriaMessage = defineMessage({ id: 'gui.aria.editMenu', defaultMessage: 'Edit menu', description: 'accessibility label for edit menu' @@ -51,7 +51,7 @@ const EditMenu = ({ [styles.active]: isExpanded() })} onClick={handleOnOpen} - aria-label={intl.formatMessage(editMenu)} + aria-label={intl.formatMessage(editMenuAriaMessage)} aria-expanded={isExpanded()} onKeyDown={handleKeyPress} ref={menuRef} diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx index cb7d24e534..51817a976d 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx @@ -309,7 +309,7 @@ class MenuBar extends React.Component { )} iconClassName={styles.remixButtonIcon} iconSrc={remixIcon} - onClick={this.handleClickRemix} + onClick={this.props.onClickRemix} > {remixMessage} @@ -462,7 +462,6 @@ class MenuBar extends React.Component {
    (platform === PLATFORM.ANDROID ? scratchLogoAndroid : scratchLogo); -export const MenuBarItemTooltip = ({ +const MenuBarItemTooltip = ({ children, className, enable, diff --git a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx index c9afba333e..cf8e3f2083 100644 --- a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx +++ b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx @@ -107,7 +107,16 @@ export default function useMenuNavigation ({ e.preventDefault(); handleOnClose(); } - }, [handleMove, handleOnClose]); + + if (e.key === KEY.ENTER) { + e.preventDefault(); + e.stopPropagation(); + const focusedRef = itemRefs[focusedIndex]; + if (focusedRef?.current) { + focusedRef.current.click(); + } + } + }, [handleMove, handleOnClose, itemRefs, focusedIndex]); const handleKeyPress = useCallback(e => { if (isExpanded() && depth === 1 && e.key === KEY.TAB) { @@ -117,7 +126,8 @@ export default function useMenuNavigation ({ if (menuContext.isInnermostMenu(menuRef)) { handleKeyPressOpenMenu(e); - } else if (!isExpanded() && (e.key === KEY.SPACE || (e.key === KEY.ARROW_RIGHT && depth !== 1))) { + } else if (!isExpanded() && (e.key === KEY.SPACE || + (e.key === KEY.ARROW_RIGHT && depth !== 1))) { e.preventDefault(); handleOnOpen(); } From f80c5d7220ec727adc346dcd79dc9c537bbc0aa3 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 19 Jan 2026 12:18:02 +0200 Subject: [PATCH 42/66] feat: reimplemented algorithm for finding inner menus via HTML tag BFS --- .../src/components/menu-bar/about-menu.jsx | 13 +- .../src/components/menu-bar/account-menu.jsx | 38 +--- .../src/components/menu-bar/edit-menu.jsx | 17 +- .../src/components/menu-bar/file-menu.jsx | 36 +--- .../src/components/menu-bar/language-menu.jsx | 26 ++- .../src/components/menu-bar/menu-bar.jsx | 13 -- .../src/components/menu-bar/mode-menu.jsx | 25 +-- .../components/menu-bar/preference-menu.jsx | 29 ++- .../src/components/menu-bar/settings-menu.jsx | 30 +-- .../scratch-gui/src/components/menu/menu.jsx | 9 +- .../scratch-gui/src/containers/menu-item.jsx | 8 +- .../src/hooks/use-menu-navigation.jsx | 173 +++++++++++------- packages/scratch-gui/src/lib/prop-types.js | 9 - 13 files changed, 171 insertions(+), 255 deletions(-) delete mode 100644 packages/scratch-gui/src/lib/prop-types.js diff --git a/packages/scratch-gui/src/components/menu-bar/about-menu.jsx b/packages/scratch-gui/src/components/menu-bar/about-menu.jsx index 51e9a1fb06..432a0e829f 100644 --- a/packages/scratch-gui/src/components/menu-bar/about-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/about-menu.jsx @@ -1,4 +1,4 @@ -import React, {useCallback, useMemo} from 'react'; +import React, {useCallback} from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import {useIntl, defineMessage} from 'react-intl'; @@ -11,7 +11,6 @@ import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; import styles from './menu-bar.css'; import aboutIcon from './icon--about.svg'; -import propTypes from '../../lib/prop-types.js'; const aboutMenuMessage = defineMessage({ id: 'gui.aria.aboutMenu', @@ -36,7 +35,6 @@ AboutButton.propTypes = { }; const AboutMenu = ({ - menuRef, onClick, isRtl }) => { @@ -54,17 +52,15 @@ const AboutMenu = ({ // generate a menu with items for each object in the array const intl = useIntl(); - const itemRefs = useMemo(() => onClick.map(() => React.createRef(null)), [onClick]); const { + menuRef, isExpanded, handleOnOpen, handleOnClose, handleKeyPress, handleKeyPressOpenMenu } = useMenuNavigation({ - menuRef, - itemRefs, depth: 1 }); @@ -98,13 +94,13 @@ const AboutMenu = ({ onRequestClose={handleOnClose} > { - onClick.map((itemProps, index) => ( + onClick.map(itemProps => ( {itemProps.title} @@ -116,7 +112,6 @@ const AboutMenu = ({ }; AboutMenu.propTypes = { - menuRef: propTypes.ref.isRequired, isRtl: PropTypes.bool.isRequired, onClick: PropTypes.oneOfType([ PropTypes.func, // button mode: call this callback when the About button is clicked diff --git a/packages/scratch-gui/src/components/menu-bar/account-menu.jsx b/packages/scratch-gui/src/components/menu-bar/account-menu.jsx index 24cfdddcca..7dfde959b6 100644 --- a/packages/scratch-gui/src/components/menu-bar/account-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/account-menu.jsx @@ -1,12 +1,11 @@ import styles from './account-menu.css'; import stylesMenuBar from './menu-bar.css'; import classNames from 'classnames'; -import React, {useMemo, useRef} from 'react'; +import React from 'react'; import useMenuNavigation from '../../hooks/use-menu-navigation'; import {useIntl, FormattedMessage, defineMessage} from 'react-intl'; import {connect} from 'react-redux'; import PropTypes from 'prop-types'; -import propTypes from '../../lib/prop-types'; import {AccountMenuOptionsPropTypes} from '../../lib/account-menu-options'; @@ -23,7 +22,6 @@ const accountMenu = defineMessage({ }); const AccountMenu = ({ - menuRef, menuOpts, username, isRtl, @@ -42,31 +40,14 @@ const AccountMenu = ({ const intl = useIntl(); - const profileRef = useRef(null); - const myStuffRef = useRef(null); - const myClassesRef = useRef(null); - const myClassRef = useRef(null); - const accountSettingsRef = useRef(null); - const logoutRef = useRef(null); - - const itemRefs = useMemo(() => [ - ...(profileUrl ? [profileRef] : []), - ...(myStuffUrl ? [myStuffRef] : []), - ...(myClassesUrl ? [myClassesRef] : []), - ...(myClassUrl ? [myClassRef] : []), - ...(accountSettingsUrl ? [accountSettingsRef] : []), - ...(canLogout ? [logoutRef] : []) - ], [profileUrl, myStuffUrl, myClassesUrl, myClassUrl, accountSettingsUrl, canLogout]); - const { isExpanded, handleOnOpen, handleOnClose, handleKeyPress, - handleKeyPressOpenMenu + handleKeyPressOpenMenu, + menuRef } = useMenuNavigation({ - menuRef, - itemRefs, depth: 1 }); @@ -114,7 +95,7 @@ const AccountMenu = ({ {profileUrl ? ( { const intl = useIntl(); - const restoreRef = useRef(null); - const turboRef = useRef(null); - - const itemRefs = [restoreRef, turboRef]; - const { + menuRef, isExpanded, handleKeyPress, handleKeyPressOpenMenu, handleOnOpen, handleOnClose } = useMenuNavigation({ - menuRef, - itemRefs, depth: 1 }); @@ -75,7 +67,7 @@ const EditMenu = ({ @@ -86,7 +78,7 @@ const EditMenu = ({ {(toggleTurboMode, {turboMode}) => ( {turboMode ? ( @@ -111,7 +103,6 @@ const EditMenu = ({ }; EditMenu.propTypes = { - menuRef: propTypes.ref.isRequired, isRtl: PropTypes.bool, restoreOptionMessage: PropTypes.func.isRequired, onRestoreOption: PropTypes.func.isRequired diff --git a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx index 0e9ed23325..ba00604ad4 100644 --- a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx @@ -1,4 +1,4 @@ -import React, {useRef} from 'react'; +import React from 'react'; import styles from './menu-bar.css'; import classNames from 'classnames'; import PropTypes from 'prop-types'; @@ -13,7 +13,6 @@ import dropdownCaret from './dropdown-caret.svg'; import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; import sharedMessages from '../../lib/shared-messages'; -import propTypes from '../../lib/prop-types.js'; import { manualUpdateProject, @@ -29,7 +28,6 @@ const fileMenu = defineMessage({ const FileMenu = ({ isRtl, - menuRef, canSave, canCreateCopy, canRemix, @@ -43,31 +41,14 @@ const FileMenu = ({ }) => { const intl = useIntl(); - const newProjectRef = useRef(null); - const saveRef = useRef(null); - const createRef = useRef(null); - const remixRef = useRef(null); - const loadFromComputerRef = useRef(null); - const saveToComputerRef = useRef(null); - - const itemRefs = [ - newProjectRef, - ...(canSave ? [saveRef] : []), - ...(canCreateCopy ? [createRef] : []), - ...(canRemix ? [remixRef] : []), - loadFromComputerRef, - saveToComputerRef - ]; - const { + menuRef, isExpanded, handleKeyPress, handleKeyPressOpenMenu, handleOnOpen, handleOnClose } = useMenuNavigation({ - menuRef, - itemRefs, depth: 1 }); @@ -123,7 +104,7 @@ const FileMenu = ({ {newProjectMessage} @@ -134,7 +115,7 @@ const FileMenu = ({ {canSave && ( {saveNowMessage} @@ -143,7 +124,7 @@ const FileMenu = ({ {canCreateCopy && ( {createCopyMessage} @@ -152,7 +133,7 @@ const FileMenu = ({ {canRemix && ( {remixMessage} @@ -163,7 +144,7 @@ const FileMenu = ({ {intl.formatMessage(sharedMessages.loadFromComputerTitle)} @@ -172,7 +153,7 @@ const FileMenu = ({ { const intl = useIntl(); - const itemRefs = React.useMemo(() => Object.keys(locales).map(() => React.createRef()), []); const selectedRef = useRef(null); const { isExpanded, handleKeyPress, handleKeyPressOpenMenu, - handleOnOpen + handleOnOpen, + menuRef } = useMenuNavigation({ - menuRef, - itemRefs, depth: 2, defaultIndexOnOpen: (Object.keys(locales).indexOf(currentLocale)) }); useEffect(() => { - const selectedIndex = Object.keys(locales).indexOf(currentLocale); - if (isExpanded() && selectedIndex >= 0 && itemRefs[selectedIndex]?.current) { - itemRefs[selectedIndex].current.scrollIntoView({block: 'center'}); + if (isExpanded()) { + selectedRef.current.scrollIntoView({block: 'center'}); } - }, [currentLocale, isExpanded, itemRefs]); + }, [selectedRef, isExpanded]); const setRef = useCallback(component => { selectedRef.current = component; @@ -64,7 +59,10 @@ const LanguageMenu = ({ }, [isExpanded]); return ( - +
    {(this.props.canChangeColorMode || this.props.canChangeLanguage || this.props.canChangeTheme) && ()} {(this.props.canManageFiles) && ()} {this.props.isTotallyNormal && ( diff --git a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx index f0bf835460..8e78329c2a 100644 --- a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx @@ -1,4 +1,4 @@ -import React, {useRef} from 'react'; +import React from 'react'; import styles from './menu-bar.css'; import classNames from 'classnames'; import PropTypes from 'prop-types'; @@ -9,8 +9,6 @@ import MenuBarMenu from './menu-bar-menu.jsx'; import {MenuItem, MenuSection} from '../menu/menu.jsx'; import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; -import propTypes from '../../lib/prop-types.js'; - const EditorModes = { NOW: 'NOW', MODE_2020: '2020' @@ -26,28 +24,18 @@ const ModeMenu = ({ isRtl, mode2020, modeNow, - onSetMode, - menuRef + onSetMode }) => { const intl = useIntl(); - const normalRef = useRef(null); - const caturdayRef = useRef(null); - - const itemRefs = [ - normalRef, - caturdayRef - ]; - const { isExpanded, handleOnOpen, handleOnClose, handleKeyPress, - handleKeyPressOpenMenu + handleKeyPressOpenMenu, + menuRef } = useMenuNavigation({ - menuRef, - itemRefs, depth: 1 }); @@ -78,7 +66,7 @@ const ModeMenu = ({ @@ -93,7 +81,7 @@ const ModeMenu = ({ @@ -113,7 +101,6 @@ const ModeMenu = ({ }; ModeMenu.propTypes = { - menuRef: propTypes.ref.isRequired, onSetMode: PropTypes.func.isRequired, modeNow: PropTypes.bool.isRequired, mode2020: PropTypes.bool.isRequired, diff --git a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx index 17280b179e..d6ad94bc88 100644 --- a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; -import React, {useMemo} from 'react'; +import React from 'react'; import {FormattedMessage} from 'react-intl'; import {connect} from 'react-redux'; import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; @@ -11,7 +11,6 @@ import {MenuItem, Submenu} from '../menu/menu.jsx'; import styles from './settings-menu.css'; import dropdownCaret from './dropdown-caret.svg'; -import propTypes from '../../lib/prop-types.js'; const intlMessageShape = PropTypes.shape({ defaultMessage: PropTypes.string, @@ -26,8 +25,8 @@ const PreferenceItem = props => {
    { - const itemRefs = useMemo(() => Object.keys(itemsMap).map(() => React.createRef()), [itemsMap]); - const itemKeys = Object.keys(itemsMap); const selectedItem = itemsMap[selectedItemKey]; @@ -72,15 +67,17 @@ const PreferenceMenu = ({ isExpanded, handleKeyPress, handleKeyPressOpenMenu, - handleOnOpen + handleOnOpen, + menuRef } = useMenuNavigation({ - menuRef, - itemRefs, depth: 2 }); return ( - + - {itemKeys.map((itemKey, index) => ( + {itemKeys.map(itemKey => ( onChange(itemKey)} item={itemsMap[itemKey]} - itemRef={itemRefs[index]} />) )} @@ -119,7 +117,6 @@ const PreferenceMenu = ({ }; PreferenceMenu.propTypes = { - menuRef: propTypes.ref.isRequired, itemsMap: PropTypes.objectOf(PropTypes.shape({ icon: PropTypes.string, label: intlMessageShape.isRequired diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx index 36e92432a5..933927d762 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; -import React, {useRef, useMemo} from 'react'; +import React, {useMemo} from 'react'; import {useIntl, FormattedMessage, defineMessage} from 'react-intl'; import {connect} from 'react-redux'; import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; @@ -23,7 +23,6 @@ import dropdownCaret from './dropdown-caret.svg'; import settingsIcon from './icon--settings.svg'; import themeIcon from '../../lib/assets/icon--theme.svg'; -import propTypes from '../../lib/prop-types.js'; const settingsMenuAriaMessage = defineMessage({ id: 'gui.aria.settingsMenu', @@ -34,7 +33,6 @@ const settingsMenuAriaMessage = defineMessage({ const enabledColorModes = [DEFAULT_MODE, HIGH_CONTRAST_MODE]; const SettingsMenu = ({ - menuRef, canChangeLanguage, canChangeColorMode, canChangeTheme, @@ -62,25 +60,13 @@ const SettingsMenu = ({ }, {}), [hasActiveMembership]); const availableThemesLength = useMemo(() => Object.keys(availableThemesMap).length, [availableThemesMap]); - const languageRef = useRef(null); - const themeRef = useRef(null); - const colorRef = useRef(null); - const itemRefs = [ - ...(canChangeLanguage ? [languageRef] : []), - // TODO: If we do change the condition below for displaying the element, we have - // to change it here also. Perhaps a variable for those conditions might be of use - ...(canChangeTheme && availableThemesLength > 1 ? [themeRef] : []), - ...(canChangeColorMode ? [colorRef] : []) - ]; - const { isExpanded, handleOnOpen, handleOnClose, - handleKeyPress + handleKeyPress, + menuRef } = useMenuNavigation({ - menuRef, - itemRefs, depth: 1 }); @@ -110,16 +96,11 @@ const SettingsMenu = ({ onRequestClose={handleOnClose} > - {canChangeLanguage && } + {canChangeLanguage && } {canChangeTheme && // TODO: Consider always showing the theme menu, even if there is a single available theme availableThemesLength > 1 && } {canChangeColorMode && (
  • {children}
  • ); MenuItem.propTypes = { - itemRef: propTypes.ref, ariaLabel: PropTypes.string, ariaRole: PropTypes.string, children: PropTypes.node, @@ -104,7 +102,6 @@ MenuItem.propTypes = { onParentKeyPress: PropTypes.func }; - const addDividerClassToFirstChild = (child, id) => ( child && React.cloneElement(child, { className: classNames( diff --git a/packages/scratch-gui/src/containers/menu-item.jsx b/packages/scratch-gui/src/containers/menu-item.jsx index 29c17bf594..3609df8e9f 100644 --- a/packages/scratch-gui/src/containers/menu-item.jsx +++ b/packages/scratch-gui/src/containers/menu-item.jsx @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import {MenuItem as MenuItemComponent} from '../components/menu/menu.jsx'; -import propTypes from '../lib/prop-types.js'; class MenuItem extends React.Component { constructor (props) { @@ -20,16 +19,16 @@ class MenuItem extends React.Component { children, className, onClick, - itemRef, - onParentKeyPress + onParentKeyPress, + ...props } = this.props; const clickAction = onClick ? onClick : this.navigateToHref; return ( {children} @@ -43,7 +42,6 @@ MenuItem.propTypes = { // can take an onClick prop, or take an href and build an onClick handler href: PropTypes.string, onClick: PropTypes.func, - itemRef: propTypes.ref, onParentKeyPress: PropTypes.func }; diff --git a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx index cf8e3f2083..ccba41569e 100644 --- a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx +++ b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx @@ -1,127 +1,164 @@ -import {useCallback, useContext, useState, useEffect} from 'react'; +import {useCallback, useContext, useState, useRef} from 'react'; import {MenuRefContext} from '../contexts/menu-ref-context'; import {KEY} from '../lib/navigation-keys'; +const MENU_ITEM_SELECTOR = '[data-menu-item="true"]'; +const MENU_ITEM_WRAPPER_SELECTOR = '[data-menu-item-wrapper="true"]'; + /** - * Provides keyboard navigation and focus management logic for menu components. + * Custom hook for keyboard navigation and focus management in menu components. + * + * This hook provides: + * - Opening and closing menus + * - Handling Escape, Enter, Arrow and Tab keys + * - Coordinating nested open menus via MenuRefContext + * - Automatically focusing the first or default menu item when opening + * - BFS to find first-level menu items, including wrapped items + * + * Notes: + * - Menu items can be direct children or wrapped in a container marked with + * `data-menu-item-wrapper="true"`. The BFS will traverse until it finds menu items + * or wrapped menu items, but will not go deeper than needed. + * - The hook automatically skips the starting element itself when looking for siblings. * - * This hook encapsulates shared menu behavior such as: - * - opening and closing menus - * - moving focus between menu items with arrow keys - * - handling Escape, Enter, and Tab behavior - * - coordinating open menus via MenuRefContext * STEPS TO USE IT: - * ______________________________________________________________________________________ - * 1. Define itemRefs as an array of refs for the submenu items, then pass - * {itemRefs, menuRef (ref for component itself) and depth (starting from 1 for top-level menus)} - * 2. In the top component (button/div/...) pass - * - onClick={handleOnOpen} - * - ref={menuRef} - * - onKeyDown={handleKeyPress} - * - tabIndex={0} if it's the core menu accessible via tab or tabIndex={1} if it's a submenu - * - aria-expanded={isExpanded()} (also everywhere else where you require isExpanded() logic, - * such as the css responsible for conditionally displaying the dropdown) - * 3. In the submenu items pass: - * - itemRef={itemRefs[index]} - * - onParentKeyPress={handleKeyPressOpenMenu} - * ______________________________________________________________________________________ + * 1. In the top-level menu trigger (button/div/...) pass: + * - onClick={handleOnOpen} + * - ref={menuRef} + * - onKeyDown={handleKeyPress} + * - tabIndex={0} if it's the core menu accessible via Tab + * - aria-expanded={isExpanded()} + * 2. Mark each submenu item or leaf menu item with: + * - data-menu-item="true" + * 3. If an expandable submenu is wrapped, mark the wrapper with: + * - data-menu-item-wrapper="true" + * - see SettingsMenu -> LanguageMenu | PreferenceMenu logic for reference * @param {object} params - * Parameters object - * @param {{ current: HTMLElement | null }} params.menuRef - * Ref to the menu trigger or container element. - * @param {Array<{ current: HTMLElement | null }>} params.itemRefs - * Refs for each focusable menu item, in display order. + * Parameters object * @param {number} params.depth * Nesting depth of the menu (1 = top-level menu). - * @param {number} params.defaultIndexOnOpen - * Default menu item index to open to - * @returns {object} An object containing the focused index, menu state, and keyboard handlers: + * @param {number} [params.defaultIndexOnOpen] + * Default menu item index to focus when opening the menu. + * @returns {object} An object containing the menu state and keyboard handlers: + * - menuRef: reference to element to be used in component * - focusedIndex: number — Index of the currently focused menu item. * - isExpanded: function() — Returns true if the menu is expanded. - * - handleKeyPress: function(KeyboardEvent) — Handler for key presses on the menu. - * - handleKeyPressOpenMenu: function(KeyboardEvent) — Handler for key presses when the menu is open. + * - handleKeyPress: function(KeyboardEvent) — Handles key presses on the menu. + * - handleKeyPressOpenMenu: function(KeyboardEvent) — Handles key presses when the menu is open. * - handleOnOpen: function() — Function to open the menu. * - handleOnClose: function() — Function to close the menu. */ export default function useMenuNavigation ({ - menuRef, - itemRefs, depth, defaultIndexOnOpen = 0 }) { + const menuRef = useRef(null); const menuContext = useContext(MenuRefContext); const [focusedIndex, setFocusedIndex] = useState(-1); - const refocusRef = useCallback(ref => { - if (ref?.current) { - ref.current.focus(); - } - }, []); + // BFS to find first children with attribute + const findDirectSubitems = () => { + if (!menuRef?.current) return []; + const directSubitems = []; + // Start from the wrapper if element is inside one + const root = menuRef.current?.parentElement?.matches(MENU_ITEM_WRAPPER_SELECTOR) ? + menuRef.current.parentElement : menuRef.current; + const children = [...root.children]; - useEffect(() => { - if (focusedIndex >= 0) { - refocusRef(itemRefs[focusedIndex]); + while (children.length > 0) { + // if child is a menu item itself + const element = children.shift(); + if (element.matches(MENU_ITEM_SELECTOR)) { + // Skip original starting element if we went back to the wrapper + if (element !== menuRef.current) { + directSubitems.push(element); + } + continue; + } + + if (element.matches(MENU_ITEM_WRAPPER_SELECTOR)) { + const wrappedItems = Array.from(element.children).filter(child => + child.matches(MENU_ITEM_SELECTOR) + ); + directSubitems.push(...wrappedItems); + } else { + children.push(...element.children); + } } - }, [focusedIndex, refocusRef]); + + return directSubitems; + }; const isExpanded = useCallback( () => menuContext.isOpenMenu(menuRef), [menuContext, menuRef] ); + const refocusIndex = useCallback(index => { + const items = findDirectSubitems(menuRef); + if (items?.[index]) { + items[index].focus(); + setFocusedIndex(index); + } + }, [menuRef]); + const handleOnOpen = useCallback(() => { if (menuContext.isOpenMenu(menuRef)) return; menuContext.openInnerMenu(menuRef, depth); - setFocusedIndex(defaultIndexOnOpen); + + // Wait for the UI to be rendered before interacting with the DOM + requestAnimationFrame(() => { + refocusIndex(defaultIndexOnOpen); + }); }, [menuContext, menuRef, depth, defaultIndexOnOpen]); const handleOnClose = useCallback(() => { setFocusedIndex(-1); menuContext.closeMenuByRef(menuRef); - refocusRef(menuRef); - }, [menuContext, menuRef, refocusRef]); + menuRef?.current?.focus(); + }, [menuContext, menuRef]); const handleMove = useCallback(direction => { - // Calculate the next focused menu item index based on the direction. - // Wraps around the list so that moving past the first or last item - // loops to the other end, preventing out-of-bounds errors. - const nextIndex = - (focusedIndex + direction + itemRefs.length) % - itemRefs.length; + const items = findDirectSubitems(menuRef); + if (!items?.length) return; - setFocusedIndex(nextIndex); - }, [focusedIndex, itemRefs]); + const nextIndex = (focusedIndex + direction + items.length) % items.length; + refocusIndex(nextIndex); + }, [focusedIndex, menuRef, refocusIndex]); const handleKeyPressOpenMenu = useCallback(e => { + const items = findDirectSubitems(menuRef); + // Logic for vertical menus, will need to change when implementing for vertical - if (e.key === KEY.ARROW_DOWN) { + switch (e.key) { + case KEY.ARROW_DOWN: e.preventDefault(); handleMove(1); - } - if (e.key === KEY.ARROW_UP) { + break; + case KEY.ARROW_UP: e.preventDefault(); handleMove(-1); - } - if (e.key === KEY.ARROW_LEFT || e.key === KEY.ESCAPE) { + break; + case KEY.ESCAPE: + case KEY.ARROW_LEFT: e.preventDefault(); handleOnClose(); - } - - if (e.key === KEY.ENTER) { + break; + case KEY.ENTER: e.preventDefault(); e.stopPropagation(); - const focusedRef = itemRefs[focusedIndex]; - if (focusedRef?.current) { - focusedRef.current.click(); - } + items[focusedIndex]?.click(); + break; } - }, [handleMove, handleOnClose, itemRefs, focusedIndex]); + + }, [handleMove, handleOnClose, focusedIndex]); const handleKeyPress = useCallback(e => { if (isExpanded() && depth === 1 && e.key === KEY.TAB) { handleOnClose(); menuContext.closeAllMenus(); + return; } if (menuContext.isInnermostMenu(menuRef)) { @@ -142,12 +179,12 @@ export default function useMenuNavigation ({ ]); return { + menuRef, focusedIndex, isExpanded, handleKeyPress, handleKeyPressOpenMenu, handleOnOpen, - handleOnClose, - refocusRef + handleOnClose }; } diff --git a/packages/scratch-gui/src/lib/prop-types.js b/packages/scratch-gui/src/lib/prop-types.js deleted file mode 100644 index 4b65abd480..0000000000 --- a/packages/scratch-gui/src/lib/prop-types.js +++ /dev/null @@ -1,9 +0,0 @@ -import PropTypes from 'prop-types'; - -const propTypes = { - ref: PropTypes.shape({ - current: PropTypes.instanceOf(Element) - }) -}; - -export default propTypes; From 734a9487d5f6798d802f96b969ee436a33adcfee Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 19 Jan 2026 13:08:30 +0200 Subject: [PATCH 43/66] chore: fix unit tests --- .../scratch-gui/test/unit/components/menu-bar.test.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/scratch-gui/test/unit/components/menu-bar.test.jsx b/packages/scratch-gui/test/unit/components/menu-bar.test.jsx index f683f9052e..7232fbea84 100644 --- a/packages/scratch-gui/test/unit/components/menu-bar.test.jsx +++ b/packages/scratch-gui/test/unit/components/menu-bar.test.jsx @@ -47,14 +47,14 @@ describe('MenuBar Component', () => { test('menu bar with no About handler has no About button', () => { const {container} = renderWithIntl(getComponent()); - const button = container.querySelector('button[aria-label="About"]'); + const button = container.querySelector('button[aria-label="About menu"]'); expect(button).toBeFalsy(); }); test('menu bar with an About handler has an About button', () => { const onClickAbout = jest.fn(); const {container} = renderWithIntl(getComponent({onClickAbout})); - const button = container.querySelector('button[aria-label="About"]'); + const button = container.querySelector('button[aria-label="About menu"]'); expect(button).toBeTruthy(); }); @@ -62,7 +62,7 @@ describe('MenuBar Component', () => { test('clicking on About button calls the handler', () => { const onClickAbout = jest.fn(); const {container} = renderWithIntl(getComponent({onClickAbout})); - const button = container.querySelector('button[aria-label="About"]'); + const button = container.querySelector('button[aria-label="About menu"]'); fireEvent.click(button); expect(onClickAbout).toHaveBeenCalledTimes(1); @@ -71,7 +71,7 @@ describe('MenuBar Component', () => { test('not clicking on About button does not call the handler', () => { const onClickAbout = jest.fn(); const {container} = renderWithIntl(getComponent({onClickAbout})); - const button = container.querySelector('button[aria-label="About"]'); + const button = container.querySelector('button[aria-label="About menu"]'); expect(onClickAbout).toHaveBeenCalledTimes(0); }); From f17c40ffd1350a29420dad4d1ba6dc75016a9722 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 19 Jan 2026 13:55:33 +0200 Subject: [PATCH 44/66] chore: fixed integration tests --- .../scratch-gui/test/integration/blocks-standalone.test.js | 2 +- packages/scratch-gui/test/integration/blocks.test.js | 2 +- packages/scratch-gui/test/integration/localization.test.js | 4 ++-- packages/scratch-gui/test/integration/menu-bar.test.js | 4 ++-- packages/scratch-gui/test/integration/project-loading.test.js | 2 +- packages/scratch-gui/test/integration/project-state.test.js | 2 +- .../scratch-gui/test/integration/sb-file-uploader-hoc.test.js | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/scratch-gui/test/integration/blocks-standalone.test.js b/packages/scratch-gui/test/integration/blocks-standalone.test.js index 35afe3ed54..84926581cc 100644 --- a/packages/scratch-gui/test/integration/blocks-standalone.test.js +++ b/packages/scratch-gui/test/integration/blocks-standalone.test.js @@ -21,7 +21,7 @@ const uri = path.resolve(__dirname, '../../build/standalone.html'); let driver; -const SETTINGS_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' + +const SETTINGS_MENU_XPATH = '//button[contains(@class, "menu-bar_menu-bar-item")]' + '[*[contains(@class, "settings-menu_dropdown-label")]//*[text()="Settings"]]'; // A test suite cloned from `blocks.test.js` which acts as a way to check that the diff --git a/packages/scratch-gui/test/integration/blocks.test.js b/packages/scratch-gui/test/integration/blocks.test.js index 2521100788..1e7b4828c4 100644 --- a/packages/scratch-gui/test/integration/blocks.test.js +++ b/packages/scratch-gui/test/integration/blocks.test.js @@ -21,7 +21,7 @@ const uri = path.resolve(__dirname, '../../build/index.html'); let driver; -const SETTINGS_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' + +const SETTINGS_MENU_XPATH = '//button[contains(@class, "menu-bar_menu-bar-item")]' + '[*[contains(@class, "settings-menu_dropdown-label")]//*[text()="Settings"]]'; describe('Working with the blocks', () => { diff --git a/packages/scratch-gui/test/integration/localization.test.js b/packages/scratch-gui/test/integration/localization.test.js index 00cae10ba4..2bf032721d 100644 --- a/packages/scratch-gui/test/integration/localization.test.js +++ b/packages/scratch-gui/test/integration/localization.test.js @@ -17,9 +17,9 @@ const uri = path.resolve(__dirname, '../../build/index.html'); let driver; -const FILE_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' + +const FILE_MENU_XPATH = '//button[contains(@class, "menu-bar_menu-bar-item")]' + '[*[contains(@class, "menu-bar_collapsible-label")]//*[text()="File"]]'; -const SETTINGS_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' + +const SETTINGS_MENU_XPATH = '//button[contains(@class, "menu-bar_menu-bar-item")]' + '[*[contains(@class, "settings-menu_dropdown-label")]//*[text()="Settings"]]'; describe('Localization', () => { diff --git a/packages/scratch-gui/test/integration/menu-bar.test.js b/packages/scratch-gui/test/integration/menu-bar.test.js index 9774769138..528b0e2c9a 100644 --- a/packages/scratch-gui/test/integration/menu-bar.test.js +++ b/packages/scratch-gui/test/integration/menu-bar.test.js @@ -16,9 +16,9 @@ const uri = path.resolve(__dirname, '../../build/index.html'); let driver; -const FILE_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' + +const FILE_MENU_XPATH = '//button[contains(@class, "menu-bar_menu-bar-item")]' + '[*[contains(@class, "menu-bar_collapsible-label")]//*[text()="File"]]'; -const SETTINGS_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' + +const SETTINGS_MENU_XPATH = '//button[contains(@class, "menu-bar_menu-bar-item")]' + '[*[contains(@class, "settings-menu_dropdown-label")]//*[text()="Settings"]]'; describe('Menu bar settings', () => { diff --git a/packages/scratch-gui/test/integration/project-loading.test.js b/packages/scratch-gui/test/integration/project-loading.test.js index 697b136ef8..4167a733e0 100644 --- a/packages/scratch-gui/test/integration/project-loading.test.js +++ b/packages/scratch-gui/test/integration/project-loading.test.js @@ -16,7 +16,7 @@ const uri = path.resolve(__dirname, '../../build/index.html'); let driver; -const FILE_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' + +const FILE_MENU_XPATH = '//button[contains(@class, "menu-bar_menu-bar-item")]' + '[*[contains(@class, "menu-bar_collapsible-label")]//*[text()="File"]]'; describe('Loading scratch gui', () => { diff --git a/packages/scratch-gui/test/integration/project-state.test.js b/packages/scratch-gui/test/integration/project-state.test.js index cb1c245a49..6edecbfa42 100644 --- a/packages/scratch-gui/test/integration/project-state.test.js +++ b/packages/scratch-gui/test/integration/project-state.test.js @@ -14,7 +14,7 @@ const uri = path.resolve(__dirname, '../../build/index.html'); let driver; -const FILE_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' + +const FILE_MENU_XPATH = '//button[contains(@class, "menu-bar_menu-bar-item")]' + '[*[contains(@class, "menu-bar_collapsible-label")]//*[text()="File"]]'; describe('Project state', () => { diff --git a/packages/scratch-gui/test/integration/sb-file-uploader-hoc.test.js b/packages/scratch-gui/test/integration/sb-file-uploader-hoc.test.js index 1b38f4978d..1c69580241 100644 --- a/packages/scratch-gui/test/integration/sb-file-uploader-hoc.test.js +++ b/packages/scratch-gui/test/integration/sb-file-uploader-hoc.test.js @@ -14,7 +14,7 @@ const uri = path.resolve(__dirname, '../../build/index.html'); let driver; -const FILE_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' + +const FILE_MENU_XPATH = '//button[contains(@class, "menu-bar_menu-bar-item")]' + '[*[contains(@class, "menu-bar_collapsible-label")]//*[text()="File"]]'; describe('Loading scratch gui', () => { From dd8b41cf6582bbd642ce3393efd65159a37dd2e9 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 19 Jan 2026 14:34:00 +0200 Subject: [PATCH 45/66] chore: renamed methods --- .../src/components/menu-bar/about-menu.jsx | 8 ++++---- .../src/components/menu-bar/account-menu.jsx | 18 +++++++++--------- .../src/components/menu-bar/edit-menu.jsx | 10 +++++----- .../src/components/menu-bar/file-menu.jsx | 18 +++++++++--------- .../src/components/menu-bar/language-menu.jsx | 8 ++++---- .../src/components/menu-bar/mode-menu.jsx | 10 +++++----- .../components/menu-bar/preference-menu.jsx | 8 ++++---- .../src/components/menu-bar/settings-menu.jsx | 4 ++-- .../src/hooks/use-menu-navigation.jsx | 18 +++++++++--------- 9 files changed, 51 insertions(+), 51 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/about-menu.jsx b/packages/scratch-gui/src/components/menu-bar/about-menu.jsx index 432a0e829f..9fb43c7548 100644 --- a/packages/scratch-gui/src/components/menu-bar/about-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/about-menu.jsx @@ -58,8 +58,8 @@ const AboutMenu = ({ isExpanded, handleOnOpen, handleOnClose, - handleKeyPress, - handleKeyPressOpenMenu + handleKeyDown, + handleKeyDownOpenMenu } = useMenuNavigation({ depth: 1 }); @@ -78,7 +78,7 @@ const AboutMenu = ({ [styles.active]: isExpanded() })} onClick={handleOnOpen} - onKeyDown={handleKeyPress} + onKeyDown={handleKeyDown} aria-label={intl.formatMessage(aboutMenuMessage)} aria-expanded={isExpanded()} ref={menuRef} @@ -99,7 +99,7 @@ const AboutMenu = ({ key={itemProps.title} isRtl={isRtl} onClick={wrapAboutMenuCallback(itemProps.onClick)} - onParentKeyPress={handleKeyPressOpenMenu} + onParentKeyPress={handleKeyDownOpenMenu} data-menu-item="true" > {itemProps.title} diff --git a/packages/scratch-gui/src/components/menu-bar/account-menu.jsx b/packages/scratch-gui/src/components/menu-bar/account-menu.jsx index 7dfde959b6..79084d2f24 100644 --- a/packages/scratch-gui/src/components/menu-bar/account-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/account-menu.jsx @@ -44,8 +44,8 @@ const AccountMenu = ({ isExpanded, handleOnOpen, handleOnClose, - handleKeyPress, - handleKeyPressOpenMenu, + handleKeyDown, + handleKeyDownOpenMenu, menuRef } = useMenuNavigation({ depth: 1 @@ -61,7 +61,7 @@ const AccountMenu = ({ {[stylesMenuBar.active]: isExpanded()} )} onClick={handleOnOpen} - onKeyDown={handleKeyPress} + onKeyDown={handleKeyDown} aria-label={intl.formatMessage(accountMenu)} aria-expanded={isExpanded()} ref={menuRef} @@ -96,7 +96,7 @@ const AccountMenu = ({ @@ -68,7 +68,7 @@ const EditMenu = ({ className={classNames({[styles.disabled]: !restorable})} onClick={onRestoreOption(handleRestore)} data-menu-item="true" - onParentKeyPress={handleKeyPressOpenMenu} + onParentKeyPress={handleKeyDownOpenMenu} isDisabled={!restorable} > {restoreOptionMessage(deletedItem)} @@ -79,7 +79,7 @@ const EditMenu = ({ {turboMode ? ( @@ -105,7 +105,7 @@ const FileMenu = ({ isRtl={isRtl} onClick={onClickNew} data-menu-item="true" - onParentKeyPress={handleKeyPressOpenMenu} + onParentKeyPress={handleKeyDownOpenMenu} > {newProjectMessage} @@ -116,7 +116,7 @@ const FileMenu = ({ {saveNowMessage} @@ -125,7 +125,7 @@ const FileMenu = ({ {createCopyMessage} @@ -134,7 +134,7 @@ const FileMenu = ({ {remixMessage} @@ -145,7 +145,7 @@ const FileMenu = ({ {intl.formatMessage(sharedMessages.loadFromComputerTitle)} @@ -154,7 +154,7 @@ const FileMenu = ({ className={className} onClick={getSaveToComputerHandler(downloadProjectCallback)} data-menu-item="true" - onParentKeyPress={handleKeyPressOpenMenu} + onParentKeyPress={handleKeyDownOpenMenu} > onChangeLanguage(locale)} data-menu-item="true" - onParentKeyPress={handleKeyPressOpenMenu} + onParentKeyPress={handleKeyDownOpenMenu} isSelected={isSelected} >
    {'✓'} @@ -82,7 +82,7 @@ const ModeMenu = ({ {'✓'} diff --git a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx index d6ad94bc88..c45cee644c 100644 --- a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx @@ -65,8 +65,8 @@ const PreferenceMenu = ({ const { isExpanded, - handleKeyPress, - handleKeyPressOpenMenu, + handleKeyDown, + handleKeyDownOpenMenu, handleOnOpen, menuRef } = useMenuNavigation({ @@ -84,7 +84,7 @@ const PreferenceMenu = ({ ref={menuRef} aria-expanded={isExpanded()} tabIndex={-1} - onKeyDown={handleKeyPress} + onKeyDown={handleKeyDown} data-menu-item="true" > {itemKeys.map(itemKey => ( diff --git a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx index ccba41569e..7d038da25d 100644 --- a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx +++ b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx @@ -25,7 +25,7 @@ const MENU_ITEM_WRAPPER_SELECTOR = '[data-menu-item-wrapper="true"]'; * 1. In the top-level menu trigger (button/div/...) pass: * - onClick={handleOnOpen} * - ref={menuRef} - * - onKeyDown={handleKeyPress} + * - onKeyDown={handleKeyDown} * - tabIndex={0} if it's the core menu accessible via Tab * - aria-expanded={isExpanded()} * 2. Mark each submenu item or leaf menu item with: @@ -43,8 +43,8 @@ const MENU_ITEM_WRAPPER_SELECTOR = '[data-menu-item-wrapper="true"]'; * - menuRef: reference to element to be used in component * - focusedIndex: number — Index of the currently focused menu item. * - isExpanded: function() — Returns true if the menu is expanded. - * - handleKeyPress: function(KeyboardEvent) — Handles key presses on the menu. - * - handleKeyPressOpenMenu: function(KeyboardEvent) — Handles key presses when the menu is open. + * - handleKeyDown: function(KeyboardEvent) — Handles key presses on the menu. + * - handleKeyDownOpenMenu: function(KeyboardEvent) — Handles key presses when the menu is open. * - handleOnOpen: function() — Function to open the menu. * - handleOnClose: function() — Function to close the menu. */ @@ -127,7 +127,7 @@ export default function useMenuNavigation ({ refocusIndex(nextIndex); }, [focusedIndex, menuRef, refocusIndex]); - const handleKeyPressOpenMenu = useCallback(e => { + const handleKeyDownOpenMenu = useCallback(e => { const items = findDirectSubitems(menuRef); // Logic for vertical menus, will need to change when implementing for vertical @@ -154,7 +154,7 @@ export default function useMenuNavigation ({ }, [handleMove, handleOnClose, focusedIndex]); - const handleKeyPress = useCallback(e => { + const handleKeyDown = useCallback(e => { if (isExpanded() && depth === 1 && e.key === KEY.TAB) { handleOnClose(); menuContext.closeAllMenus(); @@ -162,7 +162,7 @@ export default function useMenuNavigation ({ } if (menuContext.isInnermostMenu(menuRef)) { - handleKeyPressOpenMenu(e); + handleKeyDownOpenMenu(e); } else if (!isExpanded() && (e.key === KEY.SPACE || (e.key === KEY.ARROW_RIGHT && depth !== 1))) { e.preventDefault(); @@ -173,7 +173,7 @@ export default function useMenuNavigation ({ menuContext, menuRef, isExpanded, - handleKeyPressOpenMenu, + handleKeyDownOpenMenu, handleOnOpen, handleOnClose ]); @@ -182,8 +182,8 @@ export default function useMenuNavigation ({ menuRef, focusedIndex, isExpanded, - handleKeyPress, - handleKeyPressOpenMenu, + handleKeyDown, + handleKeyDownOpenMenu, handleOnOpen, handleOnClose }; From c539ae6e3fa77022b77dc86b225eb2be98bb004a Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 19 Jan 2026 14:51:50 +0200 Subject: [PATCH 46/66] chore: addressing some copilot comments --- packages/scratch-gui/src/components/menu-bar/file-menu.jsx | 2 +- packages/scratch-gui/src/components/menu-bar/settings-menu.jsx | 1 - packages/scratch-gui/src/contexts/menu-ref-context.jsx | 2 +- packages/scratch-gui/src/hooks/use-menu-navigation.jsx | 1 - 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx index d75301cde9..e69502b57a 100644 --- a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx @@ -180,7 +180,7 @@ FileMenu.propTypes = { onClickRemix: PropTypes.func, onClickNew: PropTypes.func.isRequired, getSaveToComputerHandler: PropTypes.func.isRequired, - remixMessage: PropTypes.node.isRequired + remixMessage: PropTypes.node }; const mapStateToProps = state => ({ diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx index f3931cff5b..ad423c2042 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx @@ -21,7 +21,6 @@ import styles from './settings-menu.css'; import dropdownCaret from './dropdown-caret.svg'; import settingsIcon from './icon--settings.svg'; - import themeIcon from '../../lib/assets/icon--theme.svg'; const settingsMenuAriaMessage = defineMessage({ diff --git a/packages/scratch-gui/src/contexts/menu-ref-context.jsx b/packages/scratch-gui/src/contexts/menu-ref-context.jsx index 8ec6168ce2..891b2b92fe 100644 --- a/packages/scratch-gui/src/contexts/menu-ref-context.jsx +++ b/packages/scratch-gui/src/contexts/menu-ref-context.jsx @@ -54,7 +54,7 @@ export const MenuRefProvider = ({children}) => { return [...next, ref]; }); }, []); - + const closeInnerMenu = useCallback(() => { setRefStack(prev => prev.slice(0, prev.length - 1)); }, []); diff --git a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx index 7d038da25d..d93b94816f 100644 --- a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx +++ b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx @@ -75,7 +75,6 @@ export default function useMenuNavigation ({ } continue; } - if (element.matches(MENU_ITEM_WRAPPER_SELECTOR)) { const wrappedItems = Array.from(element.children).filter(child => child.matches(MENU_ITEM_SELECTOR) From b7ee8d9429552224ec10212394bba6d40de720a8 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Tue, 20 Jan 2026 13:02:30 +0200 Subject: [PATCH 47/66] chore: addressing comments, final pieces of refactoring --- .../src/components/menu-bar/about-menu.jsx | 3 +-- .../src/components/menu-bar/account-menu.css | 5 +++++ .../src/components/menu-bar/account-menu.jsx | 12 ++++++++---- .../src/components/menu-bar/author-info.jsx | 2 -- .../src/components/menu-bar/file-menu.jsx | 1 - .../src/components/menu-bar/menu-bar.jsx | 4 ---- .../src/components/menu-bar/settings-menu.jsx | 5 +++-- .../src/hooks/use-menu-navigation.jsx | 16 +++++++++++++++- 8 files changed, 32 insertions(+), 16 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/about-menu.jsx b/packages/scratch-gui/src/components/menu-bar/about-menu.jsx index 9fb43c7548..eb428a5586 100644 --- a/packages/scratch-gui/src/components/menu-bar/about-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/about-menu.jsx @@ -97,7 +97,6 @@ const AboutMenu = ({ onClick.map(itemProps => ( +
    ); }; diff --git a/packages/scratch-gui/src/components/menu-bar/author-info.jsx b/packages/scratch-gui/src/components/menu-bar/author-info.jsx index 4a17511bc5..8d3dc9e554 100644 --- a/packages/scratch-gui/src/components/menu-bar/author-info.jsx +++ b/packages/scratch-gui/src/components/menu-bar/author-info.jsx @@ -37,8 +37,6 @@ const AuthorInfo = ({ />
    {(this.props.canChangeColorMode || this.props.canChangeLanguage || this.props.canChangeTheme) && ()} {(this.props.canManageFiles) && ()} {this.props.isTotallyNormal && ( ({ activeColorMode: state.scratchGui.settings.colorMode, - activeTheme: state.scratchGui.settings.theme + activeTheme: state.scratchGui.settings.theme, + isRtl: state.locales.isRtl }); const mapDispatchToProps = dispatch => ({ diff --git a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx index d93b94816f..692770736e 100644 --- a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx +++ b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx @@ -39,6 +39,8 @@ const MENU_ITEM_WRAPPER_SELECTOR = '[data-menu-item-wrapper="true"]'; * Nesting depth of the menu (1 = top-level menu). * @param {number} [params.defaultIndexOnOpen] * Default menu item index to focus when opening the menu. + * @param {number} [params.buttonContainsMenuItems] + * Set to false in case the menu items are not children of the button for the dropdown. * @returns {object} An object containing the menu state and keyboard handlers: * - menuRef: reference to element to be used in component * - focusedIndex: number — Index of the currently focused menu item. @@ -50,7 +52,8 @@ const MENU_ITEM_WRAPPER_SELECTOR = '[data-menu-item-wrapper="true"]'; */ export default function useMenuNavigation ({ depth, - defaultIndexOnOpen = 0 + defaultIndexOnOpen = 0, + buttonContainsMenuItems = true }) { const menuRef = useRef(null); const menuContext = useContext(MenuRefContext); @@ -129,6 +132,17 @@ export default function useMenuNavigation ({ const handleKeyDownOpenMenu = useCallback(e => { const items = findDirectSubitems(menuRef); + // copies logic from handleKeyDown in case the opening clickable + // component doesn't contain its subitems as children + // it is a little bit hacky, some of the logic here could be refactored a little bit + if (!buttonContainsMenuItems) { + if (isExpanded() && depth === 1 && e.key === KEY.TAB) { + handleOnClose(); + menuContext.closeAllMenus(); + return; + } + } + // Logic for vertical menus, will need to change when implementing for vertical switch (e.key) { case KEY.ARROW_DOWN: From c1d29dd861af02b6ef59dd079d3af851c19444cb Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Wed, 21 Jan 2026 14:44:48 +0200 Subject: [PATCH 48/66] chore: improved html bfs tag logic --- .../src/components/menu-bar/about-menu.jsx | 2 +- .../src/components/menu-bar/account-menu.jsx | 14 +++++++------- .../src/components/menu-bar/edit-menu.jsx | 4 ++-- .../src/components/menu-bar/file-menu.jsx | 12 ++++++------ .../src/components/menu-bar/language-menu.jsx | 6 +++--- .../src/components/menu-bar/mode-menu.jsx | 4 ++-- .../src/components/menu-bar/preference-menu.jsx | 6 +++--- packages/scratch-gui/src/components/menu/menu.jsx | 10 +++++++--- packages/scratch-gui/src/containers/menu-item.jsx | 10 +++++++--- .../scratch-gui/src/hooks/use-menu-navigation.jsx | 11 ++++++----- 10 files changed, 44 insertions(+), 35 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/about-menu.jsx b/packages/scratch-gui/src/components/menu-bar/about-menu.jsx index eb428a5586..86cd9e0e26 100644 --- a/packages/scratch-gui/src/components/menu-bar/about-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/about-menu.jsx @@ -99,7 +99,7 @@ const AboutMenu = ({ key={itemProps.title} onClick={wrapAboutMenuCallback(itemProps.onClick)} onParentKeyPress={handleKeyDownOpenMenu} - data-menu-item="true" + isDataMenuItem > {itemProps.title} diff --git a/packages/scratch-gui/src/components/menu-bar/account-menu.jsx b/packages/scratch-gui/src/components/menu-bar/account-menu.jsx index 692d606809..5e1dafa6ed 100644 --- a/packages/scratch-gui/src/components/menu-bar/account-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/account-menu.jsx @@ -54,7 +54,7 @@ const AccountMenu = ({ return (
    +
    ) : null} -
    + ); }; diff --git a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx index 178223a2de..2b44c37e53 100644 --- a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useRef} from 'react'; +import React, {useCallback, useRef} from 'react'; import {useIntl, FormattedMessage, defineMessage} from 'react-intl'; import {connect} from 'react-redux'; import locales from 'scratch-l10n'; @@ -54,18 +54,16 @@ const LanguageMenu = ({ return (
    + {(this.props.canChangeColorMode || this.props.canChangeLanguage || this.props.canChangeTheme) && ( { @@ -94,7 +95,7 @@ const LanguageMenu = ({ return ( onChangeLanguage(locale)} isDataMenuItem @@ -102,8 +103,8 @@ const LanguageMenu = ({ isSelected={isSelected} > ul { - max-height: calc(100vh - 5rem); /* Fallback if dvh not supported */ - max-height: calc(100dvh - 5rem); -} - @media only screen and (max-width: 1024px) { .dropdown-label { display: none; From 3f38961d719b5c5b0a327c5396e4c62eece38ac2 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Fri, 23 Jan 2026 11:56:31 +0200 Subject: [PATCH 55/66] chore: fixed background bug --- packages/scratch-gui/src/components/menu-bar/settings-menu.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.css b/packages/scratch-gui/src/components/menu-bar/settings-menu.css index 3680b34009..701b54d5d0 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.css +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.css @@ -15,7 +15,7 @@ width: 100%; padding: 0; - background: inherit; + background: transparent; border: inherit; font: inherit; text-align: inherit; From 4aac3b7773594ff30bd3b8c4c3a8002e486f8c57 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Fri, 23 Jan 2026 12:37:07 +0200 Subject: [PATCH 56/66] chore: addressed copilot comments --- .../scratch-gui/src/components/menu-bar/settings-menu.jsx | 8 ++++---- packages/scratch-gui/src/contexts/menu-ref-context.jsx | 6 +++--- packages/scratch-gui/src/hooks/use-menu-navigation.jsx | 6 ++++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx index 2f4dc8863d..b2ded791f3 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx @@ -133,10 +133,10 @@ const SettingsMenu = ({ }; SettingsMenu.propTypes = { - canChangeLanguage: PropTypes.bool.isRequired, - canChangeColorMode: PropTypes.bool.isRequired, - canChangeTheme: PropTypes.bool.isRequired, - hasActiveMembership: PropTypes.bool.isRequired, + canChangeLanguage: PropTypes.bool, + canChangeColorMode: PropTypes.bool, + canChangeTheme: PropTypes.bool, + hasActiveMembership: PropTypes.bool, isRtl: PropTypes.bool, activeColorMode: PropTypes.string, onChangeColorMode: PropTypes.func, diff --git a/packages/scratch-gui/src/contexts/menu-ref-context.jsx b/packages/scratch-gui/src/contexts/menu-ref-context.jsx index 891b2b92fe..a9360c021d 100644 --- a/packages/scratch-gui/src/contexts/menu-ref-context.jsx +++ b/packages/scratch-gui/src/contexts/menu-ref-context.jsx @@ -65,10 +65,10 @@ export const MenuRefProvider = ({children}) => { const outermostMenu = useMemo(() => (refStack.length > 0 ? refStack[0] : null), [refStack]); - const isInnermostMenu = useCallback(ref => (refStack.length > 0 && - refStack[refStack.length - 1] === ref), [refStack]); + const isInnermostMenu = useCallback(ref => refStack.length > 0 && + refStack[refStack.length - 1] === ref, [refStack]); - const isOpenMenu = useCallback(ref => (refStack.includes(ref)), [refStack]); + const isOpenMenu = useCallback(ref => refStack.includes(ref), [refStack]); const value = useMemo(() => ({ refStack, diff --git a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx index 96a1f2fda9..26cffcb1a7 100644 --- a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx +++ b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx @@ -97,7 +97,7 @@ export default function useMenuNavigation ({ ); const findDirectSubitemsClickable = useCallback( - () => findDirectSubitems().map(([_, child]) => child), + () => findDirectSubitems().map(([wrapper, child]) => child), [findDirectSubitems] ); @@ -164,7 +164,9 @@ export default function useMenuNavigation ({ const clickableItems = findDirectSubitemsClickable(); const index = focusableItems.indexOf(focusedItem); - clickableItems[index].click(); + if (index >= 0 && clickableItems[index]) { + clickableItems[index].click(); + } break; } } From d393d77c4621cf58f837537ec8986656cd19ac70 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Tue, 27 Jan 2026 12:07:58 +0200 Subject: [PATCH 57/66] chore: addressing code mistakes - replaced needless state, brought back about menu icon style --- .../src/components/menu-bar/about-menu.css | 5 ++++ .../components/menu-bar/preference-menu.jsx | 5 ++-- .../src/components/menu-bar/settings-menu.jsx | 8 +++---- .../src/hooks/use-menu-navigation.jsx | 23 +++++++++---------- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/about-menu.css b/packages/scratch-gui/src/components/menu-bar/about-menu.css index e69de29bb2..f79e22810f 100644 --- a/packages/scratch-gui/src/components/menu-bar/about-menu.css +++ b/packages/scratch-gui/src/components/menu-bar/about-menu.css @@ -0,0 +1,5 @@ +.about-icon { + height: 1.25rem; + margin: 0.5rem; + vertical-align: middle; +} diff --git a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx index dea4385f59..f0192ed08a 100644 --- a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx @@ -26,7 +26,7 @@ const PreferenceItem = props => { onClick={props.onClick} onParentKeyDown={props.onParentKeyDown} isSelected={props.isSelected} - {...props} + isDataMenuItem={props.isDataMenuItem} >
    @@ -98,7 +96,7 @@ const SettingsMenu = ({ onRequestClose={handleOnClose} > - {canChangeLanguage && } + {canChangeLanguage && } {canChangeTheme && // TODO: Consider always showing the theme menu, even if there is a single available theme availableThemesLength > 1 && @@ -113,7 +111,7 @@ const SettingsMenu = ({ }} selectedItemKey={activeTheme} isRtl={isRtl} - depth={2} + depth={depth + 1} />} {canChangeColorMode && } diff --git a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx index 26cffcb1a7..2d4a7100c7 100644 --- a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx +++ b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx @@ -1,4 +1,4 @@ -import {useCallback, useContext, useState, useRef} from 'react'; +import {useCallback, useContext, useRef} from 'react'; import {MenuRefContext} from '../contexts/menu-ref-context'; import {KEY} from '../lib/navigation-keys'; @@ -25,7 +25,7 @@ const MENU_ITEM_WRAPPER_SELECTOR = '[data-menu-item-wrapper="true"]'; * - onClick={handleOnOpen} * - ref={menuRef} * - onKeyDown={handleKeyDown} - * - tabIndex={0} or {-1} depending on the kind of focusability we want + * - Make sure the element is focusable * - aria-expanded={isExpanded()} (and use it wherever else needed) * - for menu items pass onKeyDown={handleKeyDownOpenMenu} * 2. For the sake of consistent code structure, it is recommended for the core menus (depth 1) @@ -56,7 +56,6 @@ export default function useMenuNavigation ({ }) { const menuRef = useRef(null); const menuContext = useContext(MenuRefContext); - const [focusedItem, setFocusedItem] = useState(null); // BFS to find first children with attribute const findDirectSubitems = useCallback(() => { @@ -70,8 +69,8 @@ export default function useMenuNavigation ({ const element = children.shift(); if (element.matches(MENU_ITEM_SELECTOR)) { // Skip original starting element if we went back to the wrapper - if (!(menuRef.current.matches(MENU_ITEM_WRAPPER_SELECTOR) && - Array.from(menuRef.current.children).includes(element))) { + if (!(root.matches(MENU_ITEM_WRAPPER_SELECTOR) && + Array.from(root.children).includes(element))) { directSubitems.push([element, element]); } continue; @@ -109,7 +108,6 @@ export default function useMenuNavigation ({ const focusItem = useCallback(item => { if (item) { item.focus(); - setFocusedItem(item); } }, []); @@ -126,7 +124,6 @@ export default function useMenuNavigation ({ }, [menuContext, menuRef, depth, defaultIndexOnOpen]); const handleOnClose = useCallback(() => { - setFocusedItem(null); menuContext.closeMenuByRef(menuRef); menuRef?.current?.focus(); }, [menuContext, menuRef]); @@ -135,25 +132,28 @@ export default function useMenuNavigation ({ const items = findDirectSubitemsFocusable(); if (!items.length) return; - const currentIndex = items.indexOf(focusedItem); + const currentIndex = items.indexOf(document.activeElement); const nextIndex = (currentIndex + direction + items.length) % items.length; focusItem(items[nextIndex]); - }, [focusedItem, menuRef, focusItem]); + }, [menuRef, focusItem]); const handleKeyDownOpenMenu = useCallback(e => { // Logic for vertical menus, will need to change when implementing for horizontal switch (e.key) { case KEY.ARROW_DOWN: e.preventDefault(); + e.stopPropagation(); handleMove(1); break; case KEY.ARROW_UP: e.preventDefault(); + e.stopPropagation(); handleMove(-1); break; case KEY.ESCAPE: case KEY.ARROW_LEFT: e.preventDefault(); + e.stopPropagation(); handleOnClose(); break; case KEY.ENTER: @@ -162,8 +162,8 @@ export default function useMenuNavigation ({ { const focusableItems = findDirectSubitemsFocusable(); const clickableItems = findDirectSubitemsClickable(); - - const index = focusableItems.indexOf(focusedItem); + + const index = focusableItems.indexOf(document.activeElement); if (index >= 0 && clickableItems[index]) { clickableItems[index].click(); } @@ -199,7 +199,6 @@ export default function useMenuNavigation ({ return { menuRef, - focusedItem, isExpanded, handleKeyDown, handleKeyDownOpenMenu, From 783dbafec9fe4f2b78a5a5e05da2e122e98a6e36 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Wed, 28 Jan 2026 12:34:21 +0200 Subject: [PATCH 58/66] chore: added reverse arrow key navigation on isRtl condition to hook --- .../src/components/menu-bar/about-menu.jsx | 3 ++- .../src/components/menu-bar/account-menu.jsx | 3 ++- .../src/components/menu-bar/edit-menu.jsx | 3 ++- .../src/components/menu-bar/file-menu.jsx | 3 ++- .../src/components/menu-bar/language-menu.jsx | 3 ++- .../src/components/menu-bar/mode-menu.jsx | 3 ++- .../components/menu-bar/preference-menu.jsx | 3 ++- .../src/components/menu-bar/settings-menu.jsx | 3 ++- .../src/hooks/use-menu-navigation.jsx | 23 +++++++++++++++---- 9 files changed, 35 insertions(+), 12 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/about-menu.jsx b/packages/scratch-gui/src/components/menu-bar/about-menu.jsx index f9e40ba44f..c188ddbcc8 100644 --- a/packages/scratch-gui/src/components/menu-bar/about-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/about-menu.jsx @@ -63,7 +63,8 @@ const AboutMenu = ({ handleKeyDown, handleKeyDownOpenMenu } = useMenuNavigation({ - depth + depth, + isRtl }); const wrapAboutMenuCallback = useCallback( diff --git a/packages/scratch-gui/src/components/menu-bar/account-menu.jsx b/packages/scratch-gui/src/components/menu-bar/account-menu.jsx index 9f1b50e177..807cb908ee 100644 --- a/packages/scratch-gui/src/components/menu-bar/account-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/account-menu.jsx @@ -49,7 +49,8 @@ const AccountMenu = ({ handleKeyDownOpenMenu, menuRef } = useMenuNavigation({ - depth + depth, + isRtl }); return ( diff --git a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx index 02eb94038a..42b0a42c98 100644 --- a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx @@ -35,7 +35,8 @@ const EditMenu = ({ handleOnOpen, handleOnClose } = useMenuNavigation({ - depth + depth, + isRtl }); return ( diff --git a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx index 7718dc7628..76ed74912c 100644 --- a/packages/scratch-gui/src/components/menu-bar/file-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx @@ -50,7 +50,8 @@ const FileMenu = ({ handleOnOpen, handleOnClose } = useMenuNavigation({ - depth + depth, + isRtl }); const saveNowMessage = ( diff --git a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx index dab1896c47..a181f3952d 100644 --- a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx @@ -40,7 +40,8 @@ const LanguageMenu = ({ menuRef } = useMenuNavigation({ depth, - defaultIndexOnOpen: (Object.keys(locales).indexOf(currentLocale)) + defaultIndexOnOpen: (Object.keys(locales).indexOf(currentLocale)), + isRtl }); const setRef = useCallback(component => { diff --git a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx index 19cd53e016..e7999e13b0 100644 --- a/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/mode-menu.jsx @@ -37,7 +37,8 @@ const ModeMenu = ({ handleKeyDownOpenMenu, menuRef } = useMenuNavigation({ - depth + depth, + isRtl }); return ( diff --git a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx index f0192ed08a..ce1a9d74a6 100644 --- a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx @@ -72,7 +72,8 @@ const PreferenceMenu = ({ handleOnOpen, menuRef } = useMenuNavigation({ - depth + depth, + isRtl }); return ( diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx index 45f2a488c5..908fc8085f 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx @@ -67,7 +67,8 @@ const SettingsMenu = ({ handleKeyDown, menuRef } = useMenuNavigation({ - depth + depth, + isRtl }); return (
    + ) : null} - +
    ); }; diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx index de7a3acc9e..91a5475690 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx @@ -89,6 +89,11 @@ const ariaMessages = defineMessages({ id: 'gui.menuBar.home', defaultMessage: 'Home', description: 'accessibility text for the home button' + }, + myStuff: { + id: 'gui.menuBar.myStuff', + defaultMessage: 'My Stuff', + description: 'accessibility text for the my stuff button' } }); @@ -490,7 +495,10 @@ class MenuBar extends React.Component { // ************ user is logged in ************ {menuOpts.myStuffUrl ? ( - +
    { - menuContext.closeMenuByRef(menuRef); - menuRef?.current?.focus(); + /** + * Closes the menu and restores focus to the appropriate element. + * - If the menuRef is a wrapper (data-menu-item-wrapper), tries to focus its direct child + * with data-menu-item (usually the button that opened the submenu). + * - Otherwise, focuses the wrapper itself (if focusable). + * This ensures keyboard users return to the correct menu trigger after closing a submenu. + */ + const handleOnClose = useCallback(menuRefToClose => { + const ref = menuRefToClose || menuRef; + menuContext.closeMenuByRef(ref); + const wrapper = ref?.current; + + // If wrapper, try to focus the direct menu trigger button + if (wrapper && wrapper.matches(MENU_ITEM_WRAPPER_SELECTOR)) { + const directChild = Array.from(wrapper.children).find(child => + child.matches && child.matches(MENU_ITEM_SELECTOR) + ); + if (directChild) { + directChild.focus(); + return; + } + } + + // Fallback: focus wrapper itself if possible + wrapper?.focus(); }, [menuContext, menuRef]); + const handleOnCloseAllMenus = useCallback(() => { + handleOnClose(menuContext.outermostMenu); + menuContext.closeAllMenus(); + }, [handleOnClose, menuContext]); + const handleMove = useCallback(direction => { const items = findDirectSubitemsFocusable(); if (!items.length) return; @@ -179,14 +206,16 @@ export default function useMenuNavigation ({ } break; } + case KEY.TAB: + handleOnCloseAllMenus(); + break; } - }, [handleMove, handleOnClose]); + }, [handleMove, handleOnClose, handleOnCloseAllMenus]); const handleKeyDown = useCallback(e => { if (isExpanded() && e.key === KEY.TAB) { - handleOnClose(); - menuContext.closeAllMenus(); + handleOnCloseAllMenus(); return; } From 4868bad569579c6e0d966dab84e3126965de7bec Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Thu, 5 Feb 2026 15:26:46 +0200 Subject: [PATCH 60/66] chore: added some default depth --- packages/scratch-gui/src/components/menu-bar/language-menu.jsx | 2 +- .../scratch-gui/src/components/menu-bar/preference-menu.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx index a181f3952d..3eb8fa6357 100644 --- a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx @@ -39,7 +39,7 @@ const LanguageMenu = ({ handleOnOpen, menuRef } = useMenuNavigation({ - depth, + depth: depth ?? 1, defaultIndexOnOpen: (Object.keys(locales).indexOf(currentLocale)), isRtl }); diff --git a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx index ce1a9d74a6..11fa669691 100644 --- a/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/preference-menu.jsx @@ -72,7 +72,7 @@ const PreferenceMenu = ({ handleOnOpen, menuRef } = useMenuNavigation({ - depth, + depth: depth ?? 1, isRtl }); From 644990127e21e714c5d163a5563355b722776a87 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 16:33:32 +0000 Subject: [PATCH 61/66] chore(deps): update dependency webpack to v5.105.0 --- package-lock.json | 103 +++++++++++++++++---- packages/scratch-gui/package.json | 2 +- packages/scratch-render/package.json | 2 +- packages/scratch-svg-renderer/package.json | 2 +- packages/scratch-vm/package.json | 2 +- 5 files changed, 88 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 28586360f6..6d211f7edc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -270,6 +270,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2595,6 +2596,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -2636,6 +2638,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -4670,7 +4673,8 @@ "version": "0.4.1646425229", "resolved": "https://registry.npmjs.org/@mediapipe/face_detection/-/face_detection-0.4.1646425229.tgz", "integrity": "sha512-aeCN+fRAojv9ch3NXorP6r5tcGVLR3/gC1HmtqB0WEZBRXrdP6/3W/sGR0dHr1iT6ueiK95G9PVjbzFosf/hrg==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@microbit/microbit-universal-hex": { "version": "0.2.2", @@ -4747,6 +4751,7 @@ "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 16" }, @@ -5200,6 +5205,7 @@ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -8876,6 +8882,7 @@ "dev": true, "inBundle": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -9746,6 +9753,7 @@ "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-4.22.0.tgz", "integrity": "sha512-H535XtZWnWgNwSzv538czjVlbJebDl5QTMOth4RXr2p/kJ1qSIXE0vZvEtO+5EC9b00SvhplECny2yDewQb/Yg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@tensorflow/tfjs-backend-cpu": "4.22.0", "@types/offscreencanvas": "~2019.3.0", @@ -9764,6 +9772,7 @@ "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-4.22.0.tgz", "integrity": "sha512-PT43MGlnzIo+YfbsjM79Lxk9lOq6uUwZuCc8rrp0hfpLjF6Jv8jS84u2jFb+WpUeuF4K33ZDNx8CjiYrGQ2trQ==", "license": "Apache-2.0", + "peer": true, "peerDependencies": { "@tensorflow/tfjs-core": "4.22.0" } @@ -9773,6 +9782,7 @@ "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-4.22.0.tgz", "integrity": "sha512-LEkOyzbknKFoWUwfkr59vSB68DMJ4cjwwHgicXN0DUi3a0Vh1Er3JQqCI1Hl86GGZQvY8ezVrtDIvqR1ZFW55A==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@types/long": "^4.0.1", "@types/offscreencanvas": "~2019.7.0", @@ -10562,6 +10572,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -10636,6 +10647,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -10647,6 +10659,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -10859,6 +10872,7 @@ "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.3", "@typescript-eslint/types": "8.46.3", @@ -11529,6 +11543,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -11642,6 +11657,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -12028,6 +12044,7 @@ "resolved": "https://registry.npmjs.org/arraybuffer-loader/-/arraybuffer-loader-1.0.8.tgz", "integrity": "sha512-CwUVCcxCgcgZUu2w741OV6Xj1tvRVQebq22RCyGXiLgJOJ4e4M/59EPYdtK2MLfIN28t1TDvuh2ojstNq3Kh5g==", "license": "MIT", + "peer": true, "dependencies": { "loader-utils": "^1.1.0" }, @@ -12375,6 +12392,7 @@ "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "find-cache-dir": "^4.0.0", "schema-utils": "^4.0.0" @@ -12997,6 +13015,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -14597,6 +14616,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -14794,6 +14814,7 @@ "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", @@ -14968,6 +14989,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -16694,6 +16716,7 @@ "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -16851,6 +16874,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -17155,6 +17179,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -17735,6 +17760,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -18204,6 +18230,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -22846,6 +22873,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -24531,6 +24559,7 @@ "integrity": "sha512-cG1NtMWO9hWpqRNRR3dSvEQa8bFI6iLlqU2x4kwX51FQjp0qus8T9aBaAO6iGp3DeBrhdwuKxckknohkmfvsFw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "abab": "^2.0.0", "acorn": "^6.0.4", @@ -26048,6 +26077,7 @@ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -30726,6 +30756,7 @@ "dev": true, "inBundle": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -32471,6 +32502,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -32553,6 +32585,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -32772,6 +32805,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -33008,6 +33042,7 @@ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -33348,6 +33383,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -33437,6 +33473,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -33691,6 +33728,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.1", "@types/hoist-non-react-statics": "^3.3.1", @@ -33783,6 +33821,7 @@ "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-9.0.2.tgz", "integrity": "sha512-+4CCab7z8G8glgJoRjAwocsgsv6VA2w7JPxFWHRc7kvz8mec1/K5LutNC2MG28Mn8mu6+bu04XZxHv5gyfT7xQ==", "license": "MIT", + "peer": true, "dependencies": { "hyphenate-style-name": "^1.0.0", "matchmediaquery": "^0.3.0", @@ -33815,6 +33854,7 @@ "resolved": "https://registry.npmjs.org/react-style-proptype/-/react-style-proptype-3.2.2.tgz", "integrity": "sha512-ywYLSjNkxKHiZOqNlso9PZByNEY+FTyh3C+7uuziK0xFXu9xzdyfHwg4S9iyiRRoPCR4k2LqaBBsWVmSBwCWYQ==", "license": "MIT", + "peer": true, "dependencies": { "prop-types": "^15.5.4" } @@ -34604,6 +34644,7 @@ "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", "license": "Apache-2.0", + "peer": true, "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -34929,6 +34970,7 @@ "integrity": "sha512-Qd9c2p0XKZdgT5AYd+KgAMggJ8ZmCs3JnS9PTMWkyUfteKlfmKtxJbWTHkVakxwXs1Ub7jrRYVeFeF7N0sQxyw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@oxc-project/types": "=0.101.0", "@rolldown/pluginutils": "1.0.0-beta.53" @@ -35356,6 +35398,7 @@ "version": "1.0.252", "resolved": "https://registry.npmjs.org/scratch-render-fonts/-/scratch-render-fonts-1.0.252.tgz", "integrity": "sha512-leYCgtHMIqy36KqjraAiwaPYc9Bjy2L8J+vZ/CEnUE2PVP3z0dDoA4akz42/hk44kpVDzD574Th3SANt+PlLVA==", + "peer": true, "dependencies": { "base64-loader": "^1.0.0" } @@ -35461,7 +35504,8 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/seek-bzip": { "version": "1.0.6", @@ -35525,6 +35569,7 @@ "integrity": "sha512-WRgl5GcypwramYX4HV+eQGzUbD7UUbljVmS+5G1uMwX/wLgYuJAxGeerXJDMO2xshng4+FXqCgyB5QfClV6WjA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", @@ -37631,6 +37676,7 @@ "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", "license": "MIT", + "peer": true, "engines": { "node": ">= 18.12.0" }, @@ -38138,6 +38184,7 @@ "integrity": "sha512-agJabQ9P4iW+CshG8B/5OORwySzU3yaLhCSzHNGSIwvhVlXcN0coMYdBuKLo1kCZM3X2D24pa1tO7NcUqVyNyQ==", "dev": true, "license": "BlueOak-1.0.0", + "peer": true, "dependencies": { "@tapjs/processinfo": "^3.1.9", "@tapjs/stack": "4.3.0", @@ -38933,6 +38980,7 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -39245,6 +39293,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -39504,6 +39553,7 @@ "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", @@ -39593,6 +39643,7 @@ "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.0.0", @@ -39652,6 +39703,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -39885,7 +39937,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tslog": { "version": "4.10.2", @@ -39904,6 +39957,7 @@ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -40425,6 +40479,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -40898,6 +40953,7 @@ "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "loader-utils": "^2.0.0", "mime-types": "^2.1.27", @@ -40926,6 +40982,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -41204,6 +41261,7 @@ "integrity": "sha512-LYzdNAjRHhF2yA4JUQm/QyARyi216N2rpJ0lJZb8E9FU2y5v6Vk+xq/U4XBOxMefpWixT5H3TslmAHm1rqIq2w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@oxc-project/runtime": "0.101.0", "fdir": "^6.5.0", @@ -41298,6 +41356,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -41311,6 +41370,7 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", @@ -41462,9 +41522,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", @@ -41505,10 +41565,11 @@ "license": "BSD-2-Clause" }, "node_modules/webpack": { - "version": "5.104.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", - "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -41520,7 +41581,7 @@ "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.4", + "enhanced-resolve": "^5.19.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -41533,7 +41594,7 @@ "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.16", - "watchpack": "^2.4.4", + "watchpack": "^2.5.1", "webpack-sources": "^3.3.3" }, "bin": { @@ -41558,6 +41619,7 @@ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -41794,13 +41856,13 @@ "license": "MIT" }, "node_modules/webpack/node_modules/enhanced-resolve": { - "version": "5.18.4", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", - "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -42334,6 +42396,7 @@ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -42455,6 +42518,7 @@ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -42599,7 +42663,7 @@ "ts-loader": "9.5.4", "url-loader": "4.1.1", "web-audio-test-api": "0.5.2", - "webpack": "5.104.1", + "webpack": "5.105.0", "webpack-cli": "5.1.4", "webpack-dev-server": "5.2.3", "yauzl": "3.2.0" @@ -42670,7 +42734,7 @@ "tap": "21.5.0", "terser-webpack-plugin": "5.3.16", "typedoc": "0.28.16", - "webpack": "5.104.1", + "webpack": "5.105.0", "webpack-cli": "5.1.4", "webpack-dev-server": "5.2.3" }, @@ -42834,7 +42898,7 @@ "scratch-webpack-configuration": "3.1.1", "semantic-release": "25.0.3", "tap": "21.5.0", - "webpack": "5.104.1", + "webpack": "5.105.0", "webpack-cli": "5.1.4", "webpack-dev-server": "5.2.3", "xmldom": "0.1.31" @@ -42931,7 +42995,7 @@ "tap": "21.5.0", "tiny-worker": "2.3.0", "typedoc": "0.28.16", - "webpack": "5.104.1", + "webpack": "5.105.0", "webpack-cli": "4.10.0", "webpack-dev-server": "5.2.3" } @@ -43021,6 +43085,7 @@ "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^1.2.0", diff --git a/packages/scratch-gui/package.json b/packages/scratch-gui/package.json index 34db4f2719..ec16f23f7d 100644 --- a/packages/scratch-gui/package.json +++ b/packages/scratch-gui/package.json @@ -220,7 +220,7 @@ "ts-loader": "9.5.4", "url-loader": "4.1.1", "web-audio-test-api": "0.5.2", - "webpack": "5.104.1", + "webpack": "5.105.0", "webpack-cli": "5.1.4", "webpack-dev-server": "5.2.3", "yauzl": "3.2.0" diff --git a/packages/scratch-render/package.json b/packages/scratch-render/package.json index 5284abbd8d..ae5fd97571 100644 --- a/packages/scratch-render/package.json +++ b/packages/scratch-render/package.json @@ -85,7 +85,7 @@ "tap": "21.5.0", "terser-webpack-plugin": "5.3.16", "typedoc": "0.28.16", - "webpack": "5.104.1", + "webpack": "5.105.0", "webpack-cli": "5.1.4", "webpack-dev-server": "5.2.3" }, diff --git a/packages/scratch-svg-renderer/package.json b/packages/scratch-svg-renderer/package.json index e63c0d8af0..729913fe45 100644 --- a/packages/scratch-svg-renderer/package.json +++ b/packages/scratch-svg-renderer/package.json @@ -73,7 +73,7 @@ "scratch-webpack-configuration": "3.1.1", "semantic-release": "25.0.3", "tap": "21.5.0", - "webpack": "5.104.1", + "webpack": "5.105.0", "webpack-cli": "5.1.4", "webpack-dev-server": "5.2.3", "xmldom": "0.1.31" diff --git a/packages/scratch-vm/package.json b/packages/scratch-vm/package.json index adbf67640f..3fc66fbbfa 100644 --- a/packages/scratch-vm/package.json +++ b/packages/scratch-vm/package.json @@ -110,7 +110,7 @@ "tap": "21.5.0", "tiny-worker": "2.3.0", "typedoc": "0.28.16", - "webpack": "5.104.1", + "webpack": "5.105.0", "webpack-cli": "4.10.0", "webpack-dev-server": "5.2.3" } From 81d16ac24e287a988ec95fe471ca90c44eed88ad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 22:23:53 +0000 Subject: [PATCH 62/66] fix(deps): update dependency scratch-storage to v6.1.6 --- package-lock.json | 20 ++++++++++---------- packages/scratch-gui/package.json | 2 +- packages/scratch-render/package.json | 2 +- packages/scratch-vm/package.json | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6d211f7edc..6ff1011070 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35432,13 +35432,13 @@ } }, "node_modules/scratch-storage": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/scratch-storage/-/scratch-storage-6.1.5.tgz", - "integrity": "sha512-Q1rUjvvRW/TmhgEZX9g/yGwVr3t2+Q/0GYe+/ggRJolfwgLluvcZLAJHQYYYnXBca8Aoux5LxnuCuEssoizbrw==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/scratch-storage/-/scratch-storage-6.1.6.tgz", + "integrity": "sha512-R+3HCxbLE08tdIwz2ZqbHOUqeRVnMDJGacWrrCbxNsQ+kmJ6dbp3pfAN/r3Z5hFjwBRUEeI0uYIlB+7K2IsmMA==", "license": "AGPL-3.0-only", "dependencies": { "@babel/runtime": "^7.21.0", - "@scratch/task-herder": "12.6.0", + "@scratch/task-herder": "12.6.1", "arraybuffer-loader": "^1.0.3", "base64-js": "^1.3.0", "buffer": "6.0.3", @@ -35448,9 +35448,9 @@ } }, "node_modules/scratch-storage/node_modules/@scratch/task-herder": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/@scratch/task-herder/-/task-herder-12.6.0.tgz", - "integrity": "sha512-T5sO3GnNan4ffGpkNSq6NcRGQA5AIED7Oujg9kdGVnbGiHVZu8WwsRgwneTOQ99cfeADnLzWQPJpOsNAZBmMng==", + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/@scratch/task-herder/-/task-herder-12.6.1.tgz", + "integrity": "sha512-i2N15Eif1RLH1o/2Z3eIdvu65hcdDB2p7Ct9ioHI/kUJNvzGqFkjuO6e0CGIYXSq1ZWY1cdqNFCcLtqG6EAhgQ==", "license": "AGPL-3.0-only" }, "node_modules/scratch-translate-extension-languages": { @@ -42613,7 +42613,7 @@ "scratch-l10n": "6.1.60", "scratch-paint": "4.1.50", "scratch-render-fonts": "1.0.252", - "scratch-storage": "6.1.5", + "scratch-storage": "6.1.6", "startaudiocontext": "1.2.1", "style-loader": "4.0.0", "text-encoding": "0.7.0", @@ -42728,7 +42728,7 @@ "playwright-chromium": "1.58.1", "scratch-render-fonts": "1.0.252", "scratch-semantic-release-config": "4.0.1", - "scratch-storage": "6.1.5", + "scratch-storage": "6.1.6", "scratch-webpack-configuration": "3.1.1", "semantic-release": "25.0.3", "tap": "21.5.0", @@ -42961,7 +42961,7 @@ "scratch-audio": "2.0.268", "scratch-parser": "6.0.0", "scratch-sb1-converter": "2.0.279", - "scratch-storage": "6.1.5", + "scratch-storage": "6.1.6", "scratch-translate-extension-languages": "1.0.7", "text-encoding": "0.7.0", "tslog": "4.10.2", diff --git a/packages/scratch-gui/package.json b/packages/scratch-gui/package.json index ec16f23f7d..0808938516 100644 --- a/packages/scratch-gui/package.json +++ b/packages/scratch-gui/package.json @@ -170,7 +170,7 @@ "scratch-l10n": "6.1.60", "scratch-paint": "4.1.50", "scratch-render-fonts": "1.0.252", - "scratch-storage": "6.1.5", + "scratch-storage": "6.1.6", "startaudiocontext": "1.2.1", "style-loader": "4.0.0", "text-encoding": "0.7.0", diff --git a/packages/scratch-render/package.json b/packages/scratch-render/package.json index ae5fd97571..b78955b281 100644 --- a/packages/scratch-render/package.json +++ b/packages/scratch-render/package.json @@ -79,7 +79,7 @@ "playwright-chromium": "1.58.1", "scratch-render-fonts": "1.0.252", "scratch-semantic-release-config": "4.0.1", - "scratch-storage": "6.1.5", + "scratch-storage": "6.1.6", "scratch-webpack-configuration": "3.1.1", "semantic-release": "25.0.3", "tap": "21.5.0", diff --git a/packages/scratch-vm/package.json b/packages/scratch-vm/package.json index 3fc66fbbfa..271ab4b5b6 100644 --- a/packages/scratch-vm/package.json +++ b/packages/scratch-vm/package.json @@ -76,7 +76,7 @@ "scratch-audio": "2.0.268", "scratch-parser": "6.0.0", "scratch-sb1-converter": "2.0.279", - "scratch-storage": "6.1.5", + "scratch-storage": "6.1.6", "scratch-translate-extension-languages": "1.0.7", "text-encoding": "0.7.0", "tslog": "4.10.2", From 6146fecea6138a06acb2f3f2086fd9e6594a8c9d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 02:11:08 +0000 Subject: [PATCH 63/66] chore(deps): update dependency tap to v21.5.1 --- package-lock.json | 1929 ++++++++++++-------- packages/scratch-render/package.json | 2 +- packages/scratch-svg-renderer/package.json | 2 +- packages/scratch-vm/package.json | 2 +- 4 files changed, 1129 insertions(+), 806 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6ff1011070..c126ca3ce3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3758,9 +3758,9 @@ } }, "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9642,6 +9642,404 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@tapjs/after": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@tapjs/after/-/after-3.3.1.tgz", + "integrity": "sha512-/RZb0DZxfHP74ursSByTpgKU6jVUtNOtoQ3/prf76+5+G7Q7D7QIQtlrH3bUgk84DI89j+4Nc2DTkMCOLy7BWQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "is-actual-promise": "^1.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "peerDependencies": { + "@tapjs/core": "4.4.1" + } + }, + "node_modules/@tapjs/after-each": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tapjs/after-each/-/after-each-4.3.1.tgz", + "integrity": "sha512-kgRbmhKisIl31FsCxFkDmZLNj0qCdNte0aarVLsaFq1LVJOtpITdBfnuiKigrLj4Go9XiASmIpGrU8h1uYF2Xw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "function-loop": "^4.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "peerDependencies": { + "@tapjs/core": "4.4.1" + } + }, + "node_modules/@tapjs/asserts": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tapjs/asserts/-/asserts-4.3.1.tgz", + "integrity": "sha512-PyBE1/umvg/o9Ntg3gryWaamCFHhMV0zSdoD6n5saexa8AYUb9XM6XA4y7uXRisdSFVVnD8/yX0OAWsQhryE0g==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@tapjs/stack": "4.3.0", + "is-actual-promise": "^1.0.1", + "tcompare": "9.3.0", + "trivial-deferred": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "peerDependencies": { + "@tapjs/core": "4.4.1" + } + }, + "node_modules/@tapjs/before": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tapjs/before/-/before-4.3.1.tgz", + "integrity": "sha512-zxa+DrruCGJhTQCLjYa8nfyYihLsWBWCEgiSvtwOkQKNZhxcaLmH/W85zEWKJ+MnZaa4wVqkyyRkhAM12eq0Lg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "is-actual-promise": "^1.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "peerDependencies": { + "@tapjs/core": "4.4.1" + } + }, + "node_modules/@tapjs/before-each": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tapjs/before-each/-/before-each-4.3.1.tgz", + "integrity": "sha512-vPCbni80H7/6JtQY2LoO4kiRmuyOwPJXpgR2SRrH9Aq07EVveSlgMkKJxomkbuE5lGr/l6zhO/TZDPnuorSvrg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "function-loop": "^4.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "peerDependencies": { + "@tapjs/core": "4.4.1" + } + }, + "node_modules/@tapjs/chdir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@tapjs/chdir/-/chdir-3.3.1.tgz", + "integrity": "sha512-8awqiQswpJRtlOdag+wV/ezuX1kv9YKiG3DAKcNVr7exkGr61StL7qV1cdHah2rPAXlJv6blgDIYbR80d3s9qA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + }, + "peerDependencies": { + "@tapjs/core": "4.4.1" + } + }, + "node_modules/@tapjs/config": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@tapjs/config/-/config-5.4.1.tgz", + "integrity": "sha512-ZK1Zs58ALGWx6Zxd0fDlN9VlGNxoudpXZqjlr2asC/Zu6v5oyilN9CX2r9PWHyTHNe6b/TpfpOvt2gTCTpuROA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@tapjs/core": "4.4.1", + "@tapjs/test": "4.3.1", + "chalk": "^5.6.2", + "jackspeak": "^4.1.2", + "polite-json": "^5.0.0", + "tap-yaml": "4.3.0", + "walk-up-path": "^4.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "peerDependencies": { + "@tapjs/core": "4.4.1", + "@tapjs/test": "4.3.1" + } + }, + "node_modules/@tapjs/config/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@tapjs/core": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@tapjs/core/-/core-4.4.1.tgz", + "integrity": "sha512-zEeDgt6YNOKXs4NfGGZ1Lz5aLTlHNCUpwvx5hVl7CuL+/noudWZvL39Vy2rKb+zZnTSgF7b34DqGLoy8+jgpfg==", + "dev": true, + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "@tapjs/processinfo": "^3.1.9", + "@tapjs/stack": "4.3.0", + "@tapjs/test": "4.3.1", + "async-hook-domain": "^4.0.1", + "diff": "^8.0.2", + "is-actual-promise": "^1.0.1", + "minipass": "^7.0.4", + "signal-exit": "4.1", + "tap-parser": "18.3.0", + "tap-yaml": "4.3.0", + "tcompare": "9.3.0", + "trivial-deferred": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@tapjs/core/node_modules/diff": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/@tapjs/core/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@tapjs/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tapjs/error-serdes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tapjs/error-serdes/-/error-serdes-4.3.0.tgz", + "integrity": "sha512-qP266uvPm2G95ClPFpqAN6n4nicLbHrZYbZWl0UO+biOdmvjSSuxeY5f7YFygTl+UuzlyxjlRgHTq8qifnqTcw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tapjs/error-serdes/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@tapjs/filter": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tapjs/filter/-/filter-4.3.1.tgz", + "integrity": "sha512-oyoqmUcHjYvr5f7LOryVB9ruEtjTiABdwZghx3XgeRnaNiVX3J9J8/xvdctnkbDB7cq3g9Ao2DYzweDl6Zfvhg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "peerDependencies": { + "@tapjs/core": "4.4.1" + } + }, + "node_modules/@tapjs/fixture": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tapjs/fixture/-/fixture-4.3.1.tgz", + "integrity": "sha512-x3w6Ro4H6UAxNSkDTtmz73kVCjZP4TNY2m+wLLiRdi8fa3lCn7WfvHUn9zoATgRFjgOnG4XrXSkjybhqHZ4Ibw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "mkdirp": "^3.0.0", + "rimraf": "^6.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "peerDependencies": { + "@tapjs/core": "4.4.1" + } + }, + "node_modules/@tapjs/fixture/node_modules/glob": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", + "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.2", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tapjs/fixture/node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tapjs/fixture/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@tapjs/fixture/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tapjs/fixture/node_modules/rimraf": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", + "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.0", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tapjs/intercept": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tapjs/intercept/-/intercept-4.3.1.tgz", + "integrity": "sha512-AvfZwFqAh8g+226HRVMUwoHm1ncf6xMHRQfcsPPIMtjnIrbJZjr2S2uM9qTWTnlk2EVgerqfyh3H8R6ykJsGIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@tapjs/after": "3.3.1", + "@tapjs/stack": "4.3.0" + }, + "engines": { + "node": "20 || >=22" + }, + "peerDependencies": { + "@tapjs/core": "4.4.1" + } + }, + "node_modules/@tapjs/mock": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tapjs/mock/-/mock-4.3.1.tgz", + "integrity": "sha512-oiR34RhC0+h0fqLNHkDA5QmQXmVJkujvdGwUEBxR3HzUIKZWp5SfVw4dY2/Lvl33tPBOtsFDFuqnpt3+f6SrXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@tapjs/after": "3.3.1", + "@tapjs/stack": "4.3.0", + "resolve-import": "^2.1.1", + "walk-up-path": "^4.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "peerDependencies": { + "@tapjs/core": "4.4.1" + } + }, + "node_modules/@tapjs/node-serialize": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tapjs/node-serialize/-/node-serialize-4.3.1.tgz", + "integrity": "sha512-dOsTr75HFESskVvuv8L6SAw23c2WtF6aoNkaD9SwtbTQZxvZQNGMechWPWYGaMsHB+aB+6EBg1MVnqWHQrOVcw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@tapjs/error-serdes": "4.3.0", + "@tapjs/stack": "4.3.0", + "tap-parser": "18.3.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "peerDependencies": { + "@tapjs/core": "4.4.1" + } + }, "node_modules/@tapjs/processinfo": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/@tapjs/processinfo/-/processinfo-3.1.9.tgz", @@ -9672,6 +10070,560 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@tapjs/reporter": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@tapjs/reporter/-/reporter-4.4.1.tgz", + "integrity": "sha512-kOppWVcv3sa0fmsBrpzwJiUZbwEdhixBAg0J39dUDMDdNIYrefVUSJsi7f1Agi9uRRXeJfZlUw23tII4CV06rQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@tapjs/config": "5.4.1", + "@tapjs/stack": "4.3.0", + "chalk": "^5.6.2", + "ink": "^5.2.1", + "minipass": "^7.0.4", + "ms": "^2.1.3", + "patch-console": "^2.0.0", + "prismjs-terminal": "^1.2.3", + "react": "^18.2.0", + "string-length": "^6.0.0", + "tap-parser": "18.3.0", + "tap-yaml": "4.3.0", + "tcompare": "9.3.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "peerDependencies": { + "@tapjs/core": "4.4.1" + } + }, + "node_modules/@tapjs/reporter/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@tapjs/reporter/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@tapjs/reporter/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@tapjs/reporter/node_modules/string-length": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-6.0.0.tgz", + "integrity": "sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@tapjs/reporter/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@tapjs/run": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@tapjs/run/-/run-4.4.1.tgz", + "integrity": "sha512-mVD9FCknr1mkkCv1vMKL2x4pmpka8ArqHufMP8Mb3Etj6blfePNv0Mu75RWVN9bKYzKAkqPGLenDBPb9hnbUgg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@tapjs/after": "3.3.1", + "@tapjs/before": "4.3.1", + "@tapjs/config": "5.4.1", + "@tapjs/processinfo": "^3.1.9", + "@tapjs/reporter": "4.4.1", + "@tapjs/spawn": "4.3.1", + "@tapjs/stdin": "4.3.1", + "@tapjs/test": "4.3.1", + "c8": "^10.1.3", + "chalk": "^5.6.2", + "chokidar": "^4.0.2", + "foreground-child": "^4.0.0", + "glob": "^13.0.0", + "minipass": "^7.0.4", + "mkdirp": "^3.0.1", + "node-options-to-argv": "^1.0.0", + "opener": "^1.5.2", + "pacote": "^21.0.4", + "path-scurry": "^2.0.0", + "resolve-import": "^2.0.0", + "rimraf": "^6.0.0", + "semver": "^7.7.2", + "signal-exit": "^4.1.0", + "tap-parser": "18.3.0", + "tap-yaml": "4.3.0", + "tcompare": "9.3.0", + "trivial-deferred": "^2.0.0", + "which": "^5.0.0" + }, + "bin": { + "tap-run": "dist/esm/index.js" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "peerDependencies": { + "@tapjs/core": "4.4.1" + } + }, + "node_modules/@tapjs/run/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@tapjs/run/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@tapjs/run/node_modules/foreground-child": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-4.0.3.tgz", + "integrity": "sha512-yeXZaNbCBGaT9giTpLPBdtedzjwhlJBUoL/R4BVQU5mn0TQXOHwVIl1Q2DMuBIdNno4ktA1abZ7dQFVxD6uHxw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tapjs/run/node_modules/glob": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", + "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.2", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tapjs/run/node_modules/isexe": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.4.tgz", + "integrity": "sha512-jCErc4h4RnTPjFq53G4whhjAMbUAqinGrCrTT4dmMNyi4zTthK+wphqbRLJtL4BN/Mq7Zzltr0m/b1X0m7PGFQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@tapjs/run/node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tapjs/run/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@tapjs/run/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tapjs/run/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@tapjs/run/node_modules/rimraf": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", + "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.0", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tapjs/run/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tapjs/run/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tapjs/run/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@tapjs/snapshot": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tapjs/snapshot/-/snapshot-4.3.1.tgz", + "integrity": "sha512-xPE5yxnck9EhpbH2i60xIB9HxgG39wSyn6Hj+UQal/lDgprbdYNG+36Owdp52TNHOL14GcVO3aiqyOy4UdkN6A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "is-actual-promise": "^1.0.1", + "tcompare": "9.3.0", + "trivial-deferred": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "peerDependencies": { + "@tapjs/core": "4.4.1" + } + }, + "node_modules/@tapjs/spawn": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tapjs/spawn/-/spawn-4.3.1.tgz", + "integrity": "sha512-bQ5Mb0F8Vm07TDe3DYFEzuIN1aCbRyFPYjM6cD62iszT0B4znaXL4PseRXB0VoL9cxJICeNm6AiT0G9PF09z4Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + }, + "peerDependencies": { + "@tapjs/core": "4.4.1" + } + }, + "node_modules/@tapjs/stack": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tapjs/stack/-/stack-4.3.0.tgz", + "integrity": "sha512-SFASe4YaVBzMr/FXTm/QsSzbzXZOmgDNpmY3EU0JNiDCN4izHMUnoXY+Kh0EY35hx9C4JDvRjgv2MSIM7bBygg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tapjs/stdin": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tapjs/stdin/-/stdin-4.3.1.tgz", + "integrity": "sha512-wD4bJM+1LmnDkBpR7aLJ6tlwDM/OT0RaiHPFgRrpDv7FB50LeR3h9wh7s+c+Ysc9OgZDkLNLkvG9jIhb937bZA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + }, + "peerDependencies": { + "@tapjs/core": "4.4.1" + } + }, + "node_modules/@tapjs/test": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tapjs/test/-/test-4.3.1.tgz", + "integrity": "sha512-NOsYB1VhaSPmWqrvsdeVnUuklNtJwFEQnybPHjVCqq0ecjP/SZKW+nnVzt9ISAPF+FDtQisqgJV2/Y54jzhpgA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/ts-node-temp-fork-for-pr-2009": "^10.9.7", + "@tapjs/after": "3.3.1", + "@tapjs/after-each": "4.3.1", + "@tapjs/asserts": "4.3.1", + "@tapjs/before": "4.3.1", + "@tapjs/before-each": "4.3.1", + "@tapjs/chdir": "3.3.1", + "@tapjs/filter": "4.3.1", + "@tapjs/fixture": "4.3.1", + "@tapjs/intercept": "4.3.1", + "@tapjs/mock": "4.3.1", + "@tapjs/node-serialize": "4.3.1", + "@tapjs/snapshot": "4.3.1", + "@tapjs/spawn": "4.3.1", + "@tapjs/stdin": "4.3.1", + "@tapjs/typescript": "3.5.1", + "@tapjs/worker": "4.3.1", + "glob": "^13.0.0", + "jackspeak": "^4.1.2", + "mkdirp": "^3.0.0", + "package-json-from-dist": "^1.0.0", + "resolve-import": "^2.1.1", + "rimraf": "^6.0.0", + "sync-content": "^2.0.1", + "tap-parser": "18.3.0", + "tshy": "^3.1.3", + "typescript": "5.9", + "walk-up-path": "^4.0.0" + }, + "bin": { + "generate-tap-test-class": "dist/esm/build.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "peerDependencies": { + "@tapjs/core": "4.4.1" + } + }, + "node_modules/@tapjs/test/node_modules/glob": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", + "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.2", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tapjs/test/node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tapjs/test/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@tapjs/test/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tapjs/test/node_modules/rimraf": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", + "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.0", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tapjs/typescript": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@tapjs/typescript/-/typescript-3.5.1.tgz", + "integrity": "sha512-4clGpzF1OTjLZYlavI167rCseSVsLpd5ygBAf297o468VEtpRAJPYx1IjoPrnGWC/M5iJadsCrTlIuBYABw81g==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/ts-node-temp-fork-for-pr-2009": "^10.9.7" + }, + "engines": { + "node": "20 || >=22" + }, + "peerDependencies": { + "@tapjs/core": "4.4.1" + } + }, + "node_modules/@tapjs/worker": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tapjs/worker/-/worker-4.3.1.tgz", + "integrity": "sha512-RInbFaGUH+KsAl/ozRVCMhpXaQPsvwEQ7PiKprpXgKjxjUrzwlkAbFAxo4K79axCWWdm6JUD5pH93n0FJ75jYQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + }, + "peerDependencies": { + "@tapjs/core": "4.4.1" + } + }, "node_modules/@tensorflow-models/face-detection": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tensorflow-models/face-detection/-/face-detection-1.0.3.tgz", @@ -22835,13 +23787,13 @@ } }, "node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/cliui": "^8.0.2" + "@isaacs/cliui": "^9.0.0" }, "engines": { "node": "20 || >=22" @@ -22850,6 +23802,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/jackspeak/node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/java-properties": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", @@ -24732,6 +25694,15 @@ "node": ">=6" } }, + "node_modules/jsonc-simple-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jsonc-simple-parser/-/jsonc-simple-parser-3.0.0.tgz", + "integrity": "sha512-0qi9Kuj4JPar4/3b9wZteuPZrTeFzXsQyOZj7hksnReCZN3Vr17Doz7w/i3E9XH7vRkVTHhHES+r1h97I+hfww==", + "dev": true, + "dependencies": { + "reghex": "^3.0.2" + } + }, "node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", @@ -34488,6 +35459,13 @@ "node": ">=4" } }, + "node_modules/reghex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/reghex/-/reghex-3.0.2.tgz", + "integrity": "sha512-Zb9DJ5u6GhgqRSBnxV2QSnLqEwcKxHWFA1N2yUa4ZUAO1P8jlWKYtWZ6/ooV6yylspGXJX0O/uNzEv0xrCtwaA==", + "dev": true, + "license": "MIT" + }, "node_modules/registry-auth-token": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.1.tgz", @@ -37754,17 +38732,16 @@ "license": "MIT" }, "node_modules/sync-content": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/sync-content/-/sync-content-2.0.1.tgz", - "integrity": "sha512-NI1mo514yFhr8pV/5Etvgh+pSBUIpoAKoiBIUwALVlQQNAwb40bTw8hhPFaip/dvv0GhpHVOq0vq8iY02ppLTg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sync-content/-/sync-content-2.0.4.tgz", + "integrity": "sha512-w3ioiBmbaogob33WdLnuwFk+8tpePI58CTWKqtdAgEqc2hfGuSwP02gPETqNX/3PLS5skv5a1wQR0gbaa2W0XQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^11.0.0", + "glob": "^13.0.1", "mkdirp": "^3.0.1", "path-scurry": "^2.0.0", - "rimraf": "^6.0.0", - "tshy": "^3.0.0" + "rimraf": "^6.0.0" }, "bin": { "sync-content": "dist/esm/bin.mjs" @@ -37777,22 +38754,16 @@ } }, "node_modules/sync-content/node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", + "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", + "minimatch": "^10.1.2", "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, "engines": { "node": "20 || >=22" }, @@ -37801,13 +38772,13 @@ } }, "node_modules/sync-content/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "@isaacs/brace-expansion": "^5.0.1" }, "engines": { "node": "20 || >=22" @@ -37862,6 +38833,31 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sync-content/node_modules/rimraf/node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/table": { "version": "5.4.6", "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", @@ -38016,31 +39012,31 @@ } }, "node_modules/tap": { - "version": "21.5.0", - "resolved": "https://registry.npmjs.org/tap/-/tap-21.5.0.tgz", - "integrity": "sha512-nCTEk5fle5DaI9LV+PjA2r8g0o7HNg9w7Acv8PT1y3S4qZzX858nImeARVaRo3NSdXMhkMj4twhoaG9DXjt50g==", + "version": "21.5.1", + "resolved": "https://registry.npmjs.org/tap/-/tap-21.5.1.tgz", + "integrity": "sha512-uhS20sTR4Q+/T2ovawxgVLjdsTQuU+xFz9htRwlx5jwkaWiv+1xes/0ZW5IlO+hlQp9iQH3rj30FNRlnN2ZVtw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@tapjs/after": "3.3.0", - "@tapjs/after-each": "4.3.0", - "@tapjs/asserts": "4.3.0", - "@tapjs/before": "4.3.0", - "@tapjs/before-each": "4.3.0", - "@tapjs/chdir": "3.3.0", - "@tapjs/core": "4.4.0", - "@tapjs/filter": "4.3.0", - "@tapjs/fixture": "4.3.0", - "@tapjs/intercept": "4.3.0", - "@tapjs/mock": "4.3.0", - "@tapjs/node-serialize": "4.3.0", - "@tapjs/run": "4.4.0", - "@tapjs/snapshot": "4.3.0", - "@tapjs/spawn": "4.3.0", - "@tapjs/stdin": "4.3.0", - "@tapjs/test": "4.3.0", - "@tapjs/typescript": "3.5.0", - "@tapjs/worker": "4.3.0", + "@tapjs/after": "3.3.1", + "@tapjs/after-each": "4.3.1", + "@tapjs/asserts": "4.3.1", + "@tapjs/before": "4.3.1", + "@tapjs/before-each": "4.3.1", + "@tapjs/chdir": "3.3.1", + "@tapjs/core": "4.4.1", + "@tapjs/filter": "4.3.1", + "@tapjs/fixture": "4.3.1", + "@tapjs/intercept": "4.3.1", + "@tapjs/mock": "4.3.1", + "@tapjs/node-serialize": "4.3.1", + "@tapjs/run": "4.4.1", + "@tapjs/snapshot": "4.3.1", + "@tapjs/spawn": "4.3.1", + "@tapjs/stdin": "4.3.1", + "@tapjs/test": "4.3.1", + "@tapjs/typescript": "3.5.1", + "@tapjs/worker": "4.3.1", "resolve-import": "^2.1.1" }, "bin": { @@ -38053,698 +39049,7 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/tap/node_modules/@tapjs/after": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@tapjs/after/-/after-3.3.0.tgz", - "integrity": "sha512-Z8Ozhv27xPP5oaIbPA1zFHS7oozRzf4bgeRnyoDPvXIo3akhu7wq661wkxNyCbfPZ7s4ShWNbNrgwTGOowYaJg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "is-actual-promise": "^1.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "peerDependencies": { - "@tapjs/core": "4.4.0" - } - }, - "node_modules/tap/node_modules/@tapjs/after-each": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tapjs/after-each/-/after-each-4.3.0.tgz", - "integrity": "sha512-2vGA0GYGH2JcCfNSfQdyVQaeezGZ7zQRRY74WTYD7yMR7m5yHGbgJ3moPWb0MJuj+mUacos7ksXYeqXxxdGy8Q==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "function-loop": "^4.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "peerDependencies": { - "@tapjs/core": "4.4.0" - } - }, - "node_modules/tap/node_modules/@tapjs/asserts": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tapjs/asserts/-/asserts-4.3.0.tgz", - "integrity": "sha512-BuC61QMowZAtbvluWkKzD8/JF7ICqrG2xTz3sPRczvhyEvcjYHYJXqMn+gDeyPQs4LxgFpaTdaA4Xb9tPpNGJQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@tapjs/stack": "4.3.0", - "is-actual-promise": "^1.0.1", - "tcompare": "9.3.0", - "trivial-deferred": "^2.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "peerDependencies": { - "@tapjs/core": "4.4.0" - } - }, - "node_modules/tap/node_modules/@tapjs/before": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tapjs/before/-/before-4.3.0.tgz", - "integrity": "sha512-if+l+sXIeIzyJ4DusYhznetd4AbWrjOhXKkflYmPKf8F703shbpjZfwPo4jCZ2Y/rBWKHXQ1iLOm2pjebZMqJg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "is-actual-promise": "^1.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "peerDependencies": { - "@tapjs/core": "4.4.0" - } - }, - "node_modules/tap/node_modules/@tapjs/before-each": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tapjs/before-each/-/before-each-4.3.0.tgz", - "integrity": "sha512-+6qzPbXKita72BkISZF75hIqBoBNiKYrr0q7YiwzLrNi/tGlmg7pLEA83O2ttyb2MOzy9p2InTdj6tJv0t/ZBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "function-loop": "^4.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "peerDependencies": { - "@tapjs/core": "4.4.0" - } - }, - "node_modules/tap/node_modules/@tapjs/chdir": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@tapjs/chdir/-/chdir-3.3.0.tgz", - "integrity": "sha512-xR64rno2vgZct/54RSZQWb54ACF3V8LpUbRnR7f/JpHh41b1CUGQVTZTK8h+Z22U84mqS51aHJARFt25gnoivg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - }, - "peerDependencies": { - "@tapjs/core": "4.4.0" - } - }, - "node_modules/tap/node_modules/@tapjs/config": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@tapjs/config/-/config-5.4.0.tgz", - "integrity": "sha512-ycbAq6YYhm4gFnUu9wD1uDXHmJsU/IFlQAVj7LtaiSNkixjG1nLIkbi9+Kf8mfQSNa/j7m3fhAMb82jKqBLCXA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@tapjs/core": "4.4.0", - "@tapjs/test": "4.3.0", - "chalk": "^5.6.2", - "jackspeak": "^4.0.1", - "polite-json": "^5.0.0", - "tap-yaml": "4.3.0", - "walk-up-path": "^4.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "peerDependencies": { - "@tapjs/core": "4.4.0", - "@tapjs/test": "4.3.0" - } - }, - "node_modules/tap/node_modules/@tapjs/core": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@tapjs/core/-/core-4.4.0.tgz", - "integrity": "sha512-agJabQ9P4iW+CshG8B/5OORwySzU3yaLhCSzHNGSIwvhVlXcN0coMYdBuKLo1kCZM3X2D24pa1tO7NcUqVyNyQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "peer": true, - "dependencies": { - "@tapjs/processinfo": "^3.1.9", - "@tapjs/stack": "4.3.0", - "@tapjs/test": "4.3.0", - "async-hook-domain": "^4.0.1", - "diff": "^8.0.2", - "is-actual-promise": "^1.0.1", - "minipass": "^7.0.4", - "signal-exit": "4.1", - "tap-parser": "18.3.0", - "tap-yaml": "4.3.0", - "tcompare": "9.3.0", - "trivial-deferred": "^2.0.0" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/tap/node_modules/@tapjs/error-serdes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tapjs/error-serdes/-/error-serdes-4.3.0.tgz", - "integrity": "sha512-qP266uvPm2G95ClPFpqAN6n4nicLbHrZYbZWl0UO+biOdmvjSSuxeY5f7YFygTl+UuzlyxjlRgHTq8qifnqTcw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/tap/node_modules/@tapjs/filter": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tapjs/filter/-/filter-4.3.0.tgz", - "integrity": "sha512-I/Egvq7iuc9wI/OAH37OvrkO7UuVb05atxQ6Mc6MfXjrnKfzePSe32gvVcPjcbdwR4Bg0XBRI0rKozYFeXPW9g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "peerDependencies": { - "@tapjs/core": "4.4.0" - } - }, - "node_modules/tap/node_modules/@tapjs/fixture": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tapjs/fixture/-/fixture-4.3.0.tgz", - "integrity": "sha512-vFhlpF/p7NOD1HmuhouO89va3IiE6CLk47Mq1IRSU2oR8uwM1jT6BQJnCPF43P5sl2nj4fuCUomU+lVOPbvCBg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "mkdirp": "^3.0.0", - "rimraf": "^6.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "peerDependencies": { - "@tapjs/core": "4.4.0" - } - }, - "node_modules/tap/node_modules/@tapjs/intercept": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tapjs/intercept/-/intercept-4.3.0.tgz", - "integrity": "sha512-NKx3p87KL6WRG/2SwtMiJf4lecfGsTKRbmLXQyV4Fiy/HF9pCK3k+FGfU+Rk6gpa76MvdpF6IQ6uNojrAhApXg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@tapjs/after": "3.3.0", - "@tapjs/stack": "4.3.0" - }, - "engines": { - "node": "20 || >=22" - }, - "peerDependencies": { - "@tapjs/core": "4.4.0" - } - }, - "node_modules/tap/node_modules/@tapjs/mock": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tapjs/mock/-/mock-4.3.0.tgz", - "integrity": "sha512-3NndkMomhD8NJskn9ZXWkKVZAOJEzh8Vj4nSUtaOEXTfRstf9lboEQaSuvD9vju2wn8gssA7MohJ8AuADeRY5Q==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@tapjs/after": "3.3.0", - "@tapjs/stack": "4.3.0", - "resolve-import": "^2.1.1", - "walk-up-path": "^4.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "peerDependencies": { - "@tapjs/core": "4.4.0" - } - }, - "node_modules/tap/node_modules/@tapjs/node-serialize": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tapjs/node-serialize/-/node-serialize-4.3.0.tgz", - "integrity": "sha512-kBKlBlUm1XgF1yLRWUwBelZNb6oWgKDFo2tVJrWHglyDboxsEbfe7Hfgz7D9lU91nrsZ9Yh/X5xQ8sy3lP4oQw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@tapjs/error-serdes": "4.3.0", - "@tapjs/stack": "4.3.0", - "tap-parser": "18.3.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "peerDependencies": { - "@tapjs/core": "4.4.0" - } - }, - "node_modules/tap/node_modules/@tapjs/reporter": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@tapjs/reporter/-/reporter-4.4.0.tgz", - "integrity": "sha512-9oyZ55j/uwhYfqC4DXjATx8Byzmsg6AJixJdFeR+EI1xBHh7LoP8XVaJfBHgmd//g0R9gITP4omFN94jtLIxuQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@tapjs/config": "5.4.0", - "@tapjs/stack": "4.3.0", - "chalk": "^5.6.2", - "ink": "^5.2.1", - "minipass": "^7.0.4", - "ms": "^2.1.3", - "patch-console": "^2.0.0", - "prismjs-terminal": "^1.2.3", - "react": "^18.2.0", - "string-length": "^6.0.0", - "tap-parser": "18.3.0", - "tap-yaml": "4.3.0", - "tcompare": "9.3.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "peerDependencies": { - "@tapjs/core": "4.4.0" - } - }, - "node_modules/tap/node_modules/@tapjs/run": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@tapjs/run/-/run-4.4.0.tgz", - "integrity": "sha512-ejfJRr6/f5zFqrpPwMkKdqjUuXOfYIqImCgk8giFPRVtCcnQh3KYce8XcQHulx209atg149UFQr+T9ax+w+nzg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@tapjs/after": "3.3.0", - "@tapjs/before": "4.3.0", - "@tapjs/config": "5.4.0", - "@tapjs/processinfo": "^3.1.9", - "@tapjs/reporter": "4.4.0", - "@tapjs/spawn": "4.3.0", - "@tapjs/stdin": "4.3.0", - "@tapjs/test": "4.3.0", - "c8": "^10.1.3", - "chalk": "^5.6.2", - "chokidar": "^4.0.2", - "foreground-child": "^3.1.1", - "glob": "^13.0.0", - "minipass": "^7.0.4", - "mkdirp": "^3.0.1", - "node-options-to-argv": "^1.0.0", - "opener": "^1.5.2", - "pacote": "^21.0.4", - "path-scurry": "^2.0.0", - "resolve-import": "^2.0.0", - "rimraf": "^6.0.0", - "semver": "^7.7.2", - "signal-exit": "^4.1.0", - "tap-parser": "18.3.0", - "tap-yaml": "4.3.0", - "tcompare": "9.3.0", - "trivial-deferred": "^2.0.0", - "which": "^5.0.0" - }, - "bin": { - "tap-run": "dist/esm/index.js" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "peerDependencies": { - "@tapjs/core": "4.4.0" - } - }, - "node_modules/tap/node_modules/@tapjs/snapshot": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tapjs/snapshot/-/snapshot-4.3.0.tgz", - "integrity": "sha512-88g6ikMW2wNII53/5lBqN8wPVsr24yAzQZU+BkrLJ0ekosikSrQtspuCJhWMmU107AJ5FPPlO96DAFp3sUSkJg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "is-actual-promise": "^1.0.1", - "tcompare": "9.3.0", - "trivial-deferred": "^2.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "peerDependencies": { - "@tapjs/core": "4.4.0" - } - }, - "node_modules/tap/node_modules/@tapjs/spawn": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tapjs/spawn/-/spawn-4.3.0.tgz", - "integrity": "sha512-6NER3vzkJTgKFzGH71WmwjVkqgwp7x8/NXaRoEcRs5g9ItCTU4FhvUg/7mYC/EKFtSqwuJ/Q3fbJ0J541wvJSQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - }, - "peerDependencies": { - "@tapjs/core": "4.4.0" - } - }, - "node_modules/tap/node_modules/@tapjs/stack": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tapjs/stack/-/stack-4.3.0.tgz", - "integrity": "sha512-SFASe4YaVBzMr/FXTm/QsSzbzXZOmgDNpmY3EU0JNiDCN4izHMUnoXY+Kh0EY35hx9C4JDvRjgv2MSIM7bBygg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/tap/node_modules/@tapjs/stdin": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tapjs/stdin/-/stdin-4.3.0.tgz", - "integrity": "sha512-DIf9aHsAgbyDAiE1PIDPtnsX9ZSpHl7LHOgEjeitEr9LXdmgR6zAQFGVEDJW433F2UQwrysPvUZZIruM8XqjhQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - }, - "peerDependencies": { - "@tapjs/core": "4.4.0" - } - }, - "node_modules/tap/node_modules/@tapjs/test": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tapjs/test/-/test-4.3.0.tgz", - "integrity": "sha512-33+AoqqSNimlUBiyK05SGax/EiNPnhuQdUrpPRS52FdgTM6p6QeD1uqZkFOFhh5dZkoXos5YN+zHfqLN/+dD+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/ts-node-temp-fork-for-pr-2009": "^10.9.7", - "@tapjs/after": "3.3.0", - "@tapjs/after-each": "4.3.0", - "@tapjs/asserts": "4.3.0", - "@tapjs/before": "4.3.0", - "@tapjs/before-each": "4.3.0", - "@tapjs/chdir": "3.3.0", - "@tapjs/filter": "4.3.0", - "@tapjs/fixture": "4.3.0", - "@tapjs/intercept": "4.3.0", - "@tapjs/mock": "4.3.0", - "@tapjs/node-serialize": "4.3.0", - "@tapjs/snapshot": "4.3.0", - "@tapjs/spawn": "4.3.0", - "@tapjs/stdin": "4.3.0", - "@tapjs/typescript": "3.5.0", - "@tapjs/worker": "4.3.0", - "glob": "^13.0.0", - "jackspeak": "^4.0.1", - "mkdirp": "^3.0.0", - "package-json-from-dist": "^1.0.0", - "resolve-import": "^2.1.1", - "rimraf": "^6.0.0", - "sync-content": "^2.0.1", - "tap-parser": "18.3.0", - "tshy": "^3.0.3", - "typescript": "5.9", - "walk-up-path": "^4.0.0" - }, - "bin": { - "generate-tap-test-class": "dist/esm/build.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "peerDependencies": { - "@tapjs/core": "4.4.0" - } - }, - "node_modules/tap/node_modules/@tapjs/typescript": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@tapjs/typescript/-/typescript-3.5.0.tgz", - "integrity": "sha512-8v8S3QZplJ8Uzz5yNfyS6YZkiZJ80KQwIcYVjiqCBAoqVI37FrZSONd4OKiF25rxa/ZjQJzIbtGwf/toqqOZxg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/ts-node-temp-fork-for-pr-2009": "^10.9.7" - }, - "engines": { - "node": "20 || >=22" - }, - "peerDependencies": { - "@tapjs/core": "4.4.0" - } - }, - "node_modules/tap/node_modules/@tapjs/worker": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@tapjs/worker/-/worker-4.3.0.tgz", - "integrity": "sha512-8V/0/YRT5+cAodz6HQaJGFeK3YXi2WcGaTb8V1sKLna9Ll6X+vc8ngOBJHa9YqNuadw8p2eWf02nS6UKT4MOHQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - }, - "peerDependencies": { - "@tapjs/core": "4.4.0" - } - }, - "node_modules/tap/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/tap/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/tap/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/tap/node_modules/diff": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", - "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/tap/node_modules/glob": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", - "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/tap/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/tap/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/tap/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/tap/node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/tap/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/tap/node_modules/rimraf": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", - "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "glob": "^13.0.0", - "package-json-from-dist": "^1.0.1" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/tap/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tap/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/tap/node_modules/string-length": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-6.0.0.tgz", - "integrity": "sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tap/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/tap/node_modules/tap-parser": { + "node_modules/tap-parser": { "version": "18.3.0", "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-18.3.0.tgz", "integrity": "sha512-sa0M18e6RARfO0Lrm1zbQvb+7G4G/ThkFIJFvjeH1DKenl4xwyUgpRUCb5Jq64Xe086p4auiLvRzfpRjGd3Zow==", @@ -38761,7 +39066,7 @@ "node": "20 || >=22" } }, - "node_modules/tap/node_modules/tap-yaml": { + "node_modules/tap-yaml": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/tap-yaml/-/tap-yaml-4.3.0.tgz", "integrity": "sha512-48BiwXj3cUa1Lt6BLzfawJGZVihfRCY19gyjaHftQpe8ulEmB9gZW9kChQkdb0+L4YUlGWUJMpWRAJ/9bPSgVA==", @@ -38775,36 +39080,6 @@ "node": "20 || >=22" } }, - "node_modules/tap/node_modules/tcompare": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-9.3.0.tgz", - "integrity": "sha512-6kFTU2xlXNFU88/DAAIQvjBu5znTGx8QPnFtaKiLin2OtspHXyevSu0iUTZt4UrSfuRC6fIahRCqaQIhXlsTVQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "diff": "^8.0.2", - "react-element-to-jsx-string": "^15.0.0" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/tap/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/tapable": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz", @@ -38920,6 +39195,30 @@ "node": ">=8" } }, + "node_modules/tcompare": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-9.3.0.tgz", + "integrity": "sha512-6kFTU2xlXNFU88/DAAIQvjBu5znTGx8QPnFtaKiLin2OtspHXyevSu0iUTZt4UrSfuRC6fIahRCqaQIhXlsTVQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "diff": "^8.0.2", + "react-element-to-jsx-string": "^15.0.0" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/tcompare/node_modules/diff": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/temp-dir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", @@ -39779,26 +40078,27 @@ } }, "node_modules/tshy": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tshy/-/tshy-3.0.3.tgz", - "integrity": "sha512-bUX6HQCvVdPyPLy2VZuKw95CtYD5aRSEgYEK7IPV9l9xN/z284kl5/hIwOfLY/mZOOdhrO34dFOOcL1VUMVyaw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tshy/-/tshy-3.2.0.tgz", + "integrity": "sha512-/HqAZP+2lAn3P6t4IwnkseP6WPiRmv5fNirXzGA4YB15XJ3YXM7maQd1OBAEKlU3kek2iEO3VzQ/gzMhXlvAIA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "chalk": "^5.6.2", "chokidar": "^4.0.3", - "foreground-child": "^3.3.1", + "foreground-child": "^4.0.0", + "jsonc-simple-parser": "^3.0.0", "minimatch": "^10.0.3", "mkdirp": "^3.0.1", "polite-json": "^5.0.0", - "resolve-import": "^2.0.0", - "rimraf": "^6.0.1", - "sync-content": "^2.0.1", + "resolve-import": "^2.1.1", + "rimraf": "^6.1.2", + "sync-content": "^2.0.3", "typescript": "^5.9.3", "walk-up-path": "^4.0.0" }, "bin": { - "tshy": "dist/esm/index.js" + "tshy": "dist/esm/bin-min.mjs" }, "engines": { "node": "20 || >=22" @@ -39833,23 +40133,33 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/tshy/node_modules/foreground-child": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-4.0.3.tgz", + "integrity": "sha512-yeXZaNbCBGaT9giTpLPBdtedzjwhlJBUoL/R4BVQU5mn0TQXOHwVIl1Q2DMuBIdNno4ktA1abZ7dQFVxD6uHxw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/tshy/node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", + "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", + "minimatch": "^10.1.2", "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, "engines": { "node": "20 || >=22" }, @@ -39858,13 +40168,13 @@ } }, "node_modules/tshy/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "@isaacs/brace-expansion": "^5.0.1" }, "engines": { "node": "20 || >=22" @@ -39914,13 +40224,13 @@ } }, "node_modules/tshy/node_modules/rimraf": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.0.tgz", - "integrity": "sha512-DxdlA1bdNzkZK7JiNWH+BAx1x4tEJWoTofIopFo6qWUU94jYrFZ0ubY05TqH3nWPJ1nKa1JWVFDINZ3fnrle/A==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", + "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^11.0.3", + "glob": "^13.0.0", "package-json-from-dist": "^1.0.1" }, "bin": { @@ -39933,6 +40243,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/tshy/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -42731,7 +43054,7 @@ "scratch-storage": "6.1.6", "scratch-webpack-configuration": "3.1.1", "semantic-release": "25.0.3", - "tap": "21.5.0", + "tap": "21.5.1", "terser-webpack-plugin": "5.3.16", "typedoc": "0.28.16", "webpack": "5.105.0", @@ -42897,7 +43220,7 @@ "scratch-semantic-release-config": "4.0.1", "scratch-webpack-configuration": "3.1.1", "semantic-release": "25.0.3", - "tap": "21.5.0", + "tap": "21.5.1", "webpack": "5.105.0", "webpack-cli": "5.1.4", "webpack-dev-server": "5.2.3", @@ -42992,7 +43315,7 @@ "script-loader": "0.7.2", "semantic-release": "25.0.3", "stats.js": "0.17.0", - "tap": "21.5.0", + "tap": "21.5.1", "tiny-worker": "2.3.0", "typedoc": "0.28.16", "webpack": "5.105.0", diff --git a/packages/scratch-render/package.json b/packages/scratch-render/package.json index b78955b281..377cfb3727 100644 --- a/packages/scratch-render/package.json +++ b/packages/scratch-render/package.json @@ -82,7 +82,7 @@ "scratch-storage": "6.1.6", "scratch-webpack-configuration": "3.1.1", "semantic-release": "25.0.3", - "tap": "21.5.0", + "tap": "21.5.1", "terser-webpack-plugin": "5.3.16", "typedoc": "0.28.16", "webpack": "5.105.0", diff --git a/packages/scratch-svg-renderer/package.json b/packages/scratch-svg-renderer/package.json index 729913fe45..1d9cd07ba8 100644 --- a/packages/scratch-svg-renderer/package.json +++ b/packages/scratch-svg-renderer/package.json @@ -72,7 +72,7 @@ "scratch-semantic-release-config": "4.0.1", "scratch-webpack-configuration": "3.1.1", "semantic-release": "25.0.3", - "tap": "21.5.0", + "tap": "21.5.1", "webpack": "5.105.0", "webpack-cli": "5.1.4", "webpack-dev-server": "5.2.3", diff --git a/packages/scratch-vm/package.json b/packages/scratch-vm/package.json index 271ab4b5b6..21869f28d7 100644 --- a/packages/scratch-vm/package.json +++ b/packages/scratch-vm/package.json @@ -107,7 +107,7 @@ "script-loader": "0.7.2", "semantic-release": "25.0.3", "stats.js": "0.17.0", - "tap": "21.5.0", + "tap": "21.5.1", "tiny-worker": "2.3.0", "typedoc": "0.28.16", "webpack": "5.105.0", From e4ea44e936cbae9ac9717d31452271b0288d598e Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 9 Feb 2026 12:51:21 +0200 Subject: [PATCH 64/66] chore: fixed bug for focusing on the wrong element on close --- .../src/hooks/use-menu-navigation.jsx | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx index 54145d4ab5..5528e45d46 100644 --- a/packages/scratch-gui/src/hooks/use-menu-navigation.jsx +++ b/packages/scratch-gui/src/hooks/use-menu-navigation.jsx @@ -143,21 +143,7 @@ export default function useMenuNavigation ({ const handleOnClose = useCallback(menuRefToClose => { const ref = menuRefToClose || menuRef; menuContext.closeMenuByRef(ref); - const wrapper = ref?.current; - - // If wrapper, try to focus the direct menu trigger button - if (wrapper && wrapper.matches(MENU_ITEM_WRAPPER_SELECTOR)) { - const directChild = Array.from(wrapper.children).find(child => - child.matches && child.matches(MENU_ITEM_SELECTOR) - ); - if (directChild) { - directChild.focus(); - return; - } - } - - // Fallback: focus wrapper itself if possible - wrapper?.focus(); + ref?.current?.focus(); }, [menuContext, menuRef]); const handleOnCloseAllMenus = useCallback(() => { From 3ec88dd1f071c5790b4c644a72285ec67853516a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 13:31:54 +0000 Subject: [PATCH 65/66] fix(deps): update dependency scratch-storage to v6.1.7 --- package-lock.json | 23 +++++++---------------- packages/scratch-gui/package.json | 2 +- packages/scratch-render/package.json | 2 +- packages/scratch-vm/package.json | 2 +- 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index c126ca3ce3..f92576374e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25521,7 +25521,6 @@ "integrity": "sha512-cG1NtMWO9hWpqRNRR3dSvEQa8bFI6iLlqU2x4kwX51FQjp0qus8T9aBaAO6iGp3DeBrhdwuKxckknohkmfvsFw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "abab": "^2.0.0", "acorn": "^6.0.4", @@ -35622,7 +35621,6 @@ "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", "license": "Apache-2.0", - "peer": true, "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -36410,13 +36408,13 @@ } }, "node_modules/scratch-storage": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/scratch-storage/-/scratch-storage-6.1.6.tgz", - "integrity": "sha512-R+3HCxbLE08tdIwz2ZqbHOUqeRVnMDJGacWrrCbxNsQ+kmJ6dbp3pfAN/r3Z5hFjwBRUEeI0uYIlB+7K2IsmMA==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/scratch-storage/-/scratch-storage-6.1.7.tgz", + "integrity": "sha512-u2Be6Lho/NDAdPNSdOSVxrkxCzv6E4zaE+iffOXR9lizVJKvrt8bCptJu8kUijbYSVzHSdNGYza/v3v4dwpIIg==", "license": "AGPL-3.0-only", "dependencies": { "@babel/runtime": "^7.21.0", - "@scratch/task-herder": "12.6.1", + "@scratch/task-herder": "12.6.2", "arraybuffer-loader": "^1.0.3", "base64-js": "^1.3.0", "buffer": "6.0.3", @@ -36425,12 +36423,6 @@ "minilog": "^3.1.0" } }, - "node_modules/scratch-storage/node_modules/@scratch/task-herder": { - "version": "12.6.1", - "resolved": "https://registry.npmjs.org/@scratch/task-herder/-/task-herder-12.6.1.tgz", - "integrity": "sha512-i2N15Eif1RLH1o/2Z3eIdvu65hcdDB2p7Ct9ioHI/kUJNvzGqFkjuO6e0CGIYXSq1ZWY1cdqNFCcLtqG6EAhgQ==", - "license": "AGPL-3.0-only" - }, "node_modules/scratch-translate-extension-languages": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/scratch-translate-extension-languages/-/scratch-translate-extension-languages-1.0.7.tgz", @@ -40280,7 +40272,6 @@ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -42936,7 +42927,7 @@ "scratch-l10n": "6.1.60", "scratch-paint": "4.1.50", "scratch-render-fonts": "1.0.252", - "scratch-storage": "6.1.6", + "scratch-storage": "6.1.7", "startaudiocontext": "1.2.1", "style-loader": "4.0.0", "text-encoding": "0.7.0", @@ -43051,7 +43042,7 @@ "playwright-chromium": "1.58.1", "scratch-render-fonts": "1.0.252", "scratch-semantic-release-config": "4.0.1", - "scratch-storage": "6.1.6", + "scratch-storage": "6.1.7", "scratch-webpack-configuration": "3.1.1", "semantic-release": "25.0.3", "tap": "21.5.1", @@ -43284,7 +43275,7 @@ "scratch-audio": "2.0.268", "scratch-parser": "6.0.0", "scratch-sb1-converter": "2.0.279", - "scratch-storage": "6.1.6", + "scratch-storage": "6.1.7", "scratch-translate-extension-languages": "1.0.7", "text-encoding": "0.7.0", "tslog": "4.10.2", diff --git a/packages/scratch-gui/package.json b/packages/scratch-gui/package.json index 0808938516..0a0e13fe60 100644 --- a/packages/scratch-gui/package.json +++ b/packages/scratch-gui/package.json @@ -170,7 +170,7 @@ "scratch-l10n": "6.1.60", "scratch-paint": "4.1.50", "scratch-render-fonts": "1.0.252", - "scratch-storage": "6.1.6", + "scratch-storage": "6.1.7", "startaudiocontext": "1.2.1", "style-loader": "4.0.0", "text-encoding": "0.7.0", diff --git a/packages/scratch-render/package.json b/packages/scratch-render/package.json index 377cfb3727..e69e7d7281 100644 --- a/packages/scratch-render/package.json +++ b/packages/scratch-render/package.json @@ -79,7 +79,7 @@ "playwright-chromium": "1.58.1", "scratch-render-fonts": "1.0.252", "scratch-semantic-release-config": "4.0.1", - "scratch-storage": "6.1.6", + "scratch-storage": "6.1.7", "scratch-webpack-configuration": "3.1.1", "semantic-release": "25.0.3", "tap": "21.5.1", diff --git a/packages/scratch-vm/package.json b/packages/scratch-vm/package.json index 21869f28d7..930df325b6 100644 --- a/packages/scratch-vm/package.json +++ b/packages/scratch-vm/package.json @@ -76,7 +76,7 @@ "scratch-audio": "2.0.268", "scratch-parser": "6.0.0", "scratch-sb1-converter": "2.0.279", - "scratch-storage": "6.1.6", + "scratch-storage": "6.1.7", "scratch-translate-extension-languages": "1.0.7", "text-encoding": "0.7.0", "tslog": "4.10.2", From ceeb0c1b698f30b63cae55665649cd072d365afa Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Tue, 10 Feb 2026 12:34:37 +0200 Subject: [PATCH 66/66] chore: brought back old package lock --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index f92576374e..04f321742a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43523,4 +43523,4 @@ } } } -} +} \ No newline at end of file