From a2f039017d18635fb27d235256af83b795ae680c Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 15 Dec 2025 11:46:52 +0200 Subject: [PATCH 01/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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/27] 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