From a2f039017d18635fb27d235256af83b795ae680c Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Mon, 15 Dec 2025 11:46:52 +0200 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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: () => {},