Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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 = {
refStack: []
};

bindAll(this, [
'push',
'pop',
'cut',
'clear',
'isTopMenu',
'isOpenMenu'
]);
}

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 => ({
stack: 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)
};
});
}

clear () {
this.setState({refStack: []});
}

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 = {
refStack: this.state.refStack,
push: this.push,
pop: this.pop,
cut: this.cut,
clear: this.clear,
isTopMenu: this.isTopMenu,
isOpenMenu: this.isOpenMenu
};

return (
<MenuRefContext.Provider value={value}>
{this.props.children}
</MenuRefContext.Provider>
);
}
}

MenuRefProvider.propTypes = {
children: PropTypes.node
};
77 changes: 41 additions & 36 deletions packages/scratch-gui/src/components/gui/gui.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -252,42 +253,46 @@ const GUIComponent = props => {
onRequestClose={onRequestCloseBackdropLibrary}
/>
) : null}
{!menuBarHidden && <MenuBar
accountNavOpen={accountNavOpen}
authorId={authorId}
authorThumbnailUrl={authorThumbnailUrl}
authorUsername={authorUsername}
canChangeLanguage={canChangeLanguage}
canChangeTheme={canChangeTheme}
canCreateCopy={canCreateCopy}
canCreateNew={canCreateNew}
canEditTitle={canEditTitle}
canManageFiles={canManageFiles}
canRemix={canRemix}
canSave={canSave}
canShare={canShare}
className={styles.menuBarPosition}
enableCommunity={enableCommunity}
isShared={isShared}
isTotallyNormal={isTotallyNormal}
logo={logo}
renderLogin={renderLogin}
showComingSoon={showComingSoon}
onClickAbout={onClickAbout}
onClickAccountNav={onClickAccountNav}
onClickLogo={onClickLogo}
onCloseAccountNav={onCloseAccountNav}
onLogOut={onLogOut}
onOpenRegistration={onOpenRegistration}
onProjectTelemetryEvent={onProjectTelemetryEvent}
onSeeCommunity={onSeeCommunity}
onShare={onShare}
onStartSelectingFileUpload={onStartSelectingFileUpload}
onToggleLoginOpen={onToggleLoginOpen}
userOwnsProject={userOwnsProject}
username={username}
accountMenuOptions={accountMenuOptions}
/>}
{!menuBarHidden &&
<MenuRefProvider>
<MenuBar
accountNavOpen={accountNavOpen}
authorId={authorId}
authorThumbnailUrl={authorThumbnailUrl}
authorUsername={authorUsername}
canChangeLanguage={canChangeLanguage}
canChangeTheme={canChangeTheme}
canCreateCopy={canCreateCopy}
canCreateNew={canCreateNew}
canEditTitle={canEditTitle}
canManageFiles={canManageFiles}
canRemix={canRemix}
canSave={canSave}
canShare={canShare}
className={styles.menuBarPosition}
enableCommunity={enableCommunity}
isShared={isShared}
isTotallyNormal={isTotallyNormal}
logo={logo}
renderLogin={renderLogin}
showComingSoon={showComingSoon}
onClickAbout={onClickAbout}
onClickAccountNav={onClickAccountNav}
onClickLogo={onClickLogo}
onCloseAccountNav={onCloseAccountNav}
onLogOut={onLogOut}
onOpenRegistration={onOpenRegistration}
onProjectTelemetryEvent={onProjectTelemetryEvent}
onSeeCommunity={onSeeCommunity}
onShare={onShare}
onStartSelectingFileUpload={onStartSelectingFileUpload}
onToggleLoginOpen={onToggleLoginOpen}
userOwnsProject={userOwnsProject}
username={username}
accountMenuOptions={accountMenuOptions}
/>
</MenuRefProvider>
}
<Box className={boxStyles}>
<Box className={styles.flexWrapper}>
<Box className={styles.editorWrapper}>
Expand Down
121 changes: 121 additions & 0 deletions packages/scratch-gui/src/components/menu-bar/base-menu.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
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 (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);
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.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')) {
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
};

BaseMenu.defaultProps = {
onClose: () => {}
};
Loading