{ }; GUIComponent.propTypes = { - accountNavOpen: PropTypes.bool, accountMenuOptions: AccountMenuOptionsPropTypes, activeTabIndex: PropTypes.number, authorId: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), // can be false @@ -620,9 +619,7 @@ GUIComponent.propTypes = { onActivateCostumesTab: PropTypes.func, onActivateSoundsTab: PropTypes.func, onActivateTab: PropTypes.func, - onClickAccountNav: PropTypes.func, onClickLogo: PropTypes.func, - onCloseAccountNav: PropTypes.func, onExtensionButtonClick: PropTypes.func, onLogOut: PropTypes.func, onNewSpriteClick: PropTypes.func, diff --git a/packages/scratch-gui/src/components/menu-bar/about-menu.jsx b/packages/scratch-gui/src/components/menu-bar/about-menu.jsx new file mode 100644 index 0000000000..9fb43c7548 --- /dev/null +++ b/packages/scratch-gui/src/components/menu-bar/about-menu.jsx @@ -0,0 +1,131 @@ +import React, {useCallback} from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import {useIntl, defineMessage} from 'react-intl'; +import {connect} from 'react-redux'; + +import MenuBarMenu from './menu-bar-menu.jsx'; +import Button from '../button/button.jsx'; +import {MenuItem} from '../menu/menu.jsx'; +import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; + +import styles from './menu-bar.css'; +import aboutIcon from './icon--about.svg'; + +const aboutMenuMessage = defineMessage({ + id: 'gui.aria.aboutMenu', + defaultMessage: 'About menu', + description: 'accessibility label for About menu' +}); + +const AboutButton = props => { + const intl = useIntl(); + + return ( + ); +}; + +AboutMenu.propTypes = { + isRtl: PropTypes.bool.isRequired, + onClick: PropTypes.oneOfType([ + PropTypes.func, // button mode: call this callback when the About button is clicked + PropTypes.arrayOf( // menu mode: list of items in the About menu + PropTypes.shape({ + title: PropTypes.string, // text for the menu item + onClick: PropTypes.func // call this callback when the menu item is clicked + }) + ) + ]) +}; + +const mapStateToProps = state => ({ + isRtl: state.locales.isRtl +}); + +export default connect(mapStateToProps)(AboutMenu); diff --git a/packages/scratch-gui/src/components/menu-bar/account-nav.css b/packages/scratch-gui/src/components/menu-bar/account-menu.css similarity index 100% rename from packages/scratch-gui/src/components/menu-bar/account-nav.css rename to packages/scratch-gui/src/components/menu-bar/account-menu.css diff --git a/packages/scratch-gui/src/components/menu-bar/account-menu.jsx b/packages/scratch-gui/src/components/menu-bar/account-menu.jsx new file mode 100644 index 0000000000..79084d2f24 --- /dev/null +++ b/packages/scratch-gui/src/components/menu-bar/account-menu.jsx @@ -0,0 +1,199 @@ +import styles from './account-menu.css'; +import stylesMenuBar from './menu-bar.css'; +import classNames from 'classnames'; +import React from 'react'; +import useMenuNavigation from '../../hooks/use-menu-navigation'; +import {useIntl, FormattedMessage, defineMessage} from 'react-intl'; +import {connect} from 'react-redux'; +import PropTypes from 'prop-types'; + +import {AccountMenuOptionsPropTypes} from '../../lib/account-menu-options'; + +import MenuBarMenu from './menu-bar-menu.jsx'; +import {MenuSection} from '../menu/menu.jsx'; +import MenuItemContainer from '../../containers/menu-item.jsx'; +import UserAvatar from './user-avatar.jsx'; +import dropdownCaret from './dropdown-caret.svg'; + +const accountMenu = defineMessage({ + id: 'gui.aria.accountMenu', + defaultMessage: 'Account menu', + description: 'accessibility label for account menu' +}); + +const AccountMenu = ({ + menuOpts, + username, + isRtl, + onLogOut, + avatarBadge +}) => { + const { + avatarUrl, + myStuffUrl, + profileUrl, + myClassesUrl, + myClassUrl, + accountSettingsUrl, + canLogout + } = menuOpts; + + const intl = useIntl(); + + const { + isExpanded, + handleOnOpen, + handleOnClose, + handleKeyDown, + handleKeyDownOpenMenu, + menuRef + } = useMenuNavigation({ + depth: 1 + }); + + return ( + + + + {profileUrl ? ( + + + + ) : null} + + {myStuffUrl ? ( + + + + ) : null} + + {myClassesUrl ? ( + + + + ) : null} + + {myClassUrl ? ( + + + + ) : null} + + {accountSettingsUrl ? ( + + + + ) : null} + + {canLogout ? ( + + + + + + ) : null} + + + ); +}; + +AccountMenu.propTypes = { + menuOpts: AccountMenuOptionsPropTypes, + isRtl: PropTypes.bool, + username: PropTypes.string, + onLogOut: PropTypes.func, + avatarBadge: PropTypes.number +}; + +const mapStateToProps = state => ({ + isRtl: state.locales.isRtl +}); + +export default connect( + mapStateToProps +)(AccountMenu); diff --git a/packages/scratch-gui/src/components/menu-bar/account-nav.jsx b/packages/scratch-gui/src/components/menu-bar/account-nav.jsx deleted file mode 100644 index 42b5a21b47..0000000000 --- a/packages/scratch-gui/src/components/menu-bar/account-nav.jsx +++ /dev/null @@ -1,159 +0,0 @@ -/* -NOTE: this file only temporarily resides in scratch-gui. -Nearly identical code appears in scratch-www, and the two should -eventually be consolidated. -*/ - -import classNames from 'classnames'; -import {FormattedMessage} from 'react-intl'; -import PropTypes from 'prop-types'; -import React from 'react'; - -import MenuBarMenu from './menu-bar-menu.jsx'; -import {MenuSection} from '../menu/menu.jsx'; -import MenuItemContainer from '../../containers/menu-item.jsx'; -import UserAvatar from './user-avatar.jsx'; -import dropdownCaret from './dropdown-caret.svg'; - -import styles from './account-nav.css'; - -const AccountNavComponent = ({ - className, - isOpen, - isRtl, - menuBarMenuClassName, - onClick, - onClose, - onLogOut, - profileUrl, - myStuffUrl, - avatarUrl, - myClassesUrl, - myClassUrl, - accountSettingsUrl, - username, - avatarBadge -}) => ( - -
- {avatarUrl ? ( - - ) : null} - - {username} - -
- -
-
- - {profileUrl ? ( - - - - ) : null} - - {myStuffUrl ? ( - - - - ) : null} - - {myClassesUrl ? ( - - - - ) : null} - - {myClassUrl ? ( - - - - ) : null} - - {accountSettingsUrl ? ( - - - - ) : null} - - {onLogOut ? ( - - - - - - ) : null} - -
-); - -AccountNavComponent.propTypes = { - className: PropTypes.string, - - isOpen: PropTypes.bool, - isRtl: PropTypes.bool, - - menuBarMenuClassName: PropTypes.string, - - onClick: PropTypes.func, - onClose: PropTypes.func, - onLogOut: PropTypes.func, - - username: PropTypes.string, - avatarBadge: PropTypes.number, - - avatarUrl: PropTypes.string, - myStuffUrl: PropTypes.string, - profileUrl: PropTypes.string, - myClassesUrl: PropTypes.string, - myClassUrl: PropTypes.string, - accountSettingsUrl: PropTypes.string -}; - -export default AccountNavComponent; diff --git a/packages/scratch-gui/src/components/menu-bar/author-info.jsx b/packages/scratch-gui/src/components/menu-bar/author-info.jsx index c8058db7c8..4a17511bc5 100644 --- a/packages/scratch-gui/src/components/menu-bar/author-info.jsx +++ b/packages/scratch-gui/src/components/menu-bar/author-info.jsx @@ -1,11 +1,17 @@ import PropTypes from 'prop-types'; import React from 'react'; import classNames from 'classnames'; -import {FormattedMessage} from 'react-intl'; +import {FormattedMessage, defineMessage, useIntl} from 'react-intl'; import UserAvatar from './user-avatar.jsx'; import styles from './author-info.css'; +const authorInfo = defineMessage({ + id: 'gui.aria.authorInfo', + defaultMessage: 'Project "{projectTitle}" by {username}', + description: 'accessibility label for author info' +}); + const AuthorInfo = ({ className, imageUrl, @@ -14,8 +20,10 @@ const AuthorInfo = ({ userId, username, avatarBadge -}) => ( -
{ + const intl = useIntl(); + + return (
-
+
{projectTitle} @@ -49,8 +65,8 @@ const AuthorInfo = ({
-
-); +
); +}; AuthorInfo.propTypes = { className: PropTypes.string, diff --git a/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx new file mode 100644 index 0000000000..ae99c26a14 --- /dev/null +++ b/packages/scratch-gui/src/components/menu-bar/edit-menu.jsx @@ -0,0 +1,117 @@ +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 editIcon from './icon--edit.svg'; +import {useIntl, FormattedMessage, defineMessage} from 'react-intl'; +import MenuBarMenu from './menu-bar-menu.jsx'; +import {MenuItem, MenuSection} from '../menu/menu.jsx'; +import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; +import dropdownCaret from './dropdown-caret.svg'; +import DeletionRestorer from '../../containers/deletion-restorer.jsx'; +import TurboMode from '../../containers/turbo-mode.jsx'; + +const editMenuAriaMessage = defineMessage({ + id: 'gui.aria.editMenu', + defaultMessage: 'Edit menu', + description: 'accessibility label for edit menu' +}); + +const EditMenu = ({ + isRtl, + onRestoreOption, + restoreOptionMessage +}) => { + const intl = useIntl(); + + const { + menuRef, + isExpanded, + handleKeyDown, + handleKeyDownOpenMenu, + handleOnOpen, + handleOnClose + } = useMenuNavigation({ + depth: 1 + }); + + return ( + + ); +}; + +EditMenu.propTypes = { + isRtl: PropTypes.bool, + restoreOptionMessage: PropTypes.func.isRequired, + onRestoreOption: PropTypes.func.isRequired +}; + +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 new file mode 100644 index 0000000000..e69502b57a --- /dev/null +++ b/packages/scratch-gui/src/components/menu-bar/file-menu.jsx @@ -0,0 +1,199 @@ +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 fileIcon from './icon--file.svg'; +import {useIntl, FormattedMessage, defineMessage} from 'react-intl'; +import MenuBarMenu from './menu-bar-menu.jsx'; +import {MenuItem, MenuSection} from '../menu/menu.jsx'; +import SB3Downloader from '../../containers/sb3-downloader.jsx'; +import dropdownCaret from './dropdown-caret.svg'; +import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; + +import sharedMessages from '../../lib/shared-messages'; + +import { + manualUpdateProject, + remixProject, + saveProjectAsCopy +} from '../../reducers/project-state'; + +const fileMenu = defineMessage({ + id: 'gui.aria.fileMenu', + defaultMessage: 'File menu', + description: 'accessibility label for file menu' +}); + +const FileMenu = ({ + isRtl, + canSave, + canCreateCopy, + canRemix, + onClickNew, + onClickSave, + onClickSaveAsCopy, + onClickRemix, + onStartSelectingFileUpload, + getSaveToComputerHandler, + remixMessage +}) => { + const intl = useIntl(); + + const { + menuRef, + isExpanded, + handleKeyDown, + handleKeyDownOpenMenu, + handleOnOpen, + handleOnClose + } = useMenuNavigation({ + depth: 1 + }); + + const saveNowMessage = ( + + ); + const createCopyMessage = ( + + ); + const newProjectMessage = ( + + ); + + return ( + + ); +}; + +FileMenu.propTypes = { + isRtl: PropTypes.bool, + canSave: PropTypes.bool.isRequired, + canCreateCopy: PropTypes.bool.isRequired, + canRemix: PropTypes.bool.isRequired, + onStartSelectingFileUpload: PropTypes.func.isRequired, + onClickSave: PropTypes.func, + onClickSaveAsCopy: PropTypes.func, + onClickRemix: PropTypes.func, + onClickNew: PropTypes.func.isRequired, + getSaveToComputerHandler: PropTypes.func.isRequired, + remixMessage: PropTypes.node +}; + +const mapStateToProps = state => ({ + isRtl: state.locales.isRtl +}); + +const mapDispatchToProps = dispatch => ({ + onClickRemix: () => dispatch(remixProject()), + onClickSave: () => dispatch(manualUpdateProject()), + onClickSaveAsCopy: () => dispatch(saveProjectAsCopy()) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(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 81402c317a..c78b7b1d30 100644 --- a/packages/scratch-gui/src/components/menu-bar/language-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/language-menu.jsx @@ -1,127 +1,145 @@ import classNames from 'classnames'; -import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; -import React from 'react'; -import {FormattedMessage} from 'react-intl'; +import React, {useCallback, useEffect, useRef} from 'react'; +import {useIntl, FormattedMessage, defineMessage} from 'react-intl'; import {connect} from 'react-redux'; 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 {selectLocale} from '../../reducers/locales.js'; +import useMenuNavigation from '../../hooks/use-menu-navigation.jsx'; import styles from './settings-menu.css'; import dropdownCaret from './dropdown-caret.svg'; -class LanguageMenu extends React.PureComponent { - constructor (props) { - super(props); - bindAll(this, [ - 'setRef', - 'handleMouseOver' - ]); - } +const languageMenu = defineMessage({ + id: 'gui.aria.languageMenu', + defaultMessage: 'Language menu', + description: 'accessibility label for language menu' +}); + +const LanguageMenu = ({ + currentLocale, + isRtl, + onChangeLanguage +}) => { + const intl = useIntl(); + + const selectedRef = useRef(null); - 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 { + isExpanded, + handleKeyDown, + handleKeyDownOpenMenu, + handleOnOpen, + menuRef + } = useMenuNavigation({ + depth: 2, + defaultIndexOnOpen: (Object.keys(locales).indexOf(currentLocale)) + }); + + useEffect(() => { + if (isExpanded()) { + selectedRef.current.scrollIntoView({block: 'center'}); } - } + }, [selectedRef, isExpanded]); - setRef (component) { - this.selectedRef = component; - } + const setRef = useCallback(component => { + selectedRef.current = 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.current) { + selectedRef.current.scrollIntoView({block: 'center'}); } - } + }, [isExpanded]); - render () { - return ( - + + + { + Object.keys(locales) + .map(locale => { + const isSelected = currentLocale === locale; + + return ( onChangeLanguage(locale)} + data-menu-item="true" + onParentKeyPress={handleKeyDownOpenMenu} + isSelected={isSelected} + > + + {locales[locale].name} + ); + }) + } + + + ); +}; LanguageMenu.propTypes = { currentLocale: PropTypes.string, isRtl: PropTypes.bool, - label: PropTypes.string, - menuOpen: PropTypes.bool, - onChangeLanguage: PropTypes.func, - onRequestCloseSettings: PropTypes.func, - onRequestOpen: PropTypes.func + onChangeLanguage: PropTypes.func }; const mapStateToProps = state => ({ currentLocale: state.locales.locale, isRtl: state.locales.isRtl, - menuOpen: languageMenuOpen(state), messagesByLocale: state.locales.messagesByLocale }); -const mapDispatchToProps = (dispatch, ownProps) => ({ +const mapDispatchToProps = dispatch => ({ onChangeLanguage: locale => { dispatch(selectLocale(locale)); - ownProps.onRequestCloseSettings(); - }, - onRequestOpen: () => dispatch(openLanguageMenu()) + } }); export default connect( diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.css b/packages/scratch-gui/src/components/menu-bar/menu-bar.css index 68063fcf47..7aa3610348 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.css +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.css @@ -62,6 +62,12 @@ align-items: center; white-space: nowrap; height: $menu-bar-height; + + background: none; + border: none; + font: inherit; + text-align: inherit; + cursor: pointer; } .menu-bar-item.hoverable { diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx index 79b865abb4..076725e758 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx @@ -18,17 +18,15 @@ import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx'; import Divider from '../divider/divider.jsx'; import SaveStatus from './save-status.jsx'; import ProjectWatcher from '../../containers/project-watcher.jsx'; -import MenuBarMenu from './menu-bar-menu.jsx'; -import {MenuItem, MenuSection} from '../menu/menu.jsx'; import ProjectTitleInput from './project-title-input.jsx'; import AuthorInfo from './author-info.jsx'; -import AccountNav from '../../components/menu-bar/account-nav.jsx'; import LoginDropdown from './login-dropdown.jsx'; -import 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 ModeMenu from './mode-menu.jsx'; +import AboutMenu from './about-menu.jsx'; import {openTipsLibrary, openDebugModal} from '../../reducers/modals'; import {setPlayer} from '../../reducers/mode'; @@ -44,33 +42,12 @@ import { autoUpdateProject, getIsUpdating, getIsShowingProject, - manualUpdateProject, - requestNewProject, - remixProject, - saveProjectAsCopy + requestNewProject } from '../../reducers/project-state'; import { - openAboutMenu, - closeAboutMenu, - aboutMenuOpen, - openAccountMenu, - closeAccountMenu, - accountMenuOpen, - openFileMenu, - closeFileMenu, - fileMenuOpen, - openEditMenu, - closeEditMenu, - editMenuOpen, openLoginMenu, closeLoginMenu, - loginMenuOpen, - openModeMenu, - closeModeMenu, - modeMenuOpen, - settingsMenuOpen, - openSettingsMenu, - closeSettingsMenu + loginMenuOpen } from '../../reducers/menus'; import collectMetadata from '../../lib/collect-metadata'; @@ -83,9 +60,6 @@ import mystuffIcon from './icon--mystuff.png'; import profileIcon from './icon--profile.png'; import remixIcon from './icon--remix.svg'; import dropdownCaret from './dropdown-caret.svg'; -import aboutIcon from './icon--about.svg'; -import 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'; @@ -98,6 +72,7 @@ import oldtimeyLogo from './oldtimey-logo.svg'; import sharedMessages from '../../lib/shared-messages'; import {AccountMenuOptionsPropTypes} from '../../lib/account-menu-options'; +import AccountMenu from './account-menu.jsx'; const ariaMessages = defineMessages({ tutorials: { @@ -109,6 +84,11 @@ const ariaMessages = defineMessages({ id: 'gui.menuBar.debug', defaultMessage: 'Debug', description: 'accessibility text for the debug button' + }, + home: { + id: 'gui.menuBar.home', + defaultMessage: 'Home', + description: 'accessibility text for the home button' } }); @@ -140,7 +120,6 @@ const MenuBarItemTooltip = ({ ); }; - MenuBarItemTooltip.propTypes = { children: PropTypes.node, className: PropTypes.string, @@ -168,27 +147,11 @@ MenuItemTooltip.propTypes = { isRtl: PropTypes.bool }; -const AboutButton = props => ( - ); - // Show the About button only if we have a handler for it (like in the desktop app) - const aboutButton = this.buildAboutMenu(this.props.onClickAbout); const menuOpts = this.props.accountMenuOptions; @@ -445,6 +319,9 @@ class MenuBar extends React.Component {
Scratch {(this.props.canChangeColorMode || this.props.canChangeLanguage || this.props.canChangeTheme) && ()} - {(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.isTotallyNormal && ( -
-
- -
- - - - - {'✓'} - - {' '} - - - - - {'✓'} - - {' '} - - - - -
- )} + {(this.props.canManageFiles) && ()} + + {this.props.isTotallyNormal && ()}
{this.props.canEditTitle ? (
@@ -711,7 +445,7 @@ class MenuBar extends React.Component {
-
-
-
+
+
@@ -772,31 +506,12 @@ class MenuBar extends React.Component { ) : null} - ) : ( @@ -804,7 +519,7 @@ class MenuBar extends React.Component { // ********* so they can choose to log in {menuOpts.canRegister ? ( -
-
+ ) : null} {menuOpts.canLogin ? ( -
-
+ ) : null}
) @@ -895,14 +610,18 @@ class MenuBar extends React.Component { )} - {aboutButton} + {this.props.onClickAbout && ( + + )}