feat: move language and theme to submenu
This commit is contained in:
1 parent
cfab4a2c9e
commit
dd8b6faf66
19 files changed
+686
-299
No files matched your search
@@ -0,0 +1,3 @@
|
||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.64469 14.507C9.55861 14.5931 9.46145 14.6484 9.34525 14.7008L7.37873 15.5266C7.30728 15.283 7.13034 14.9734 6.80329 14.6463C6.48721 14.3302 6.18853 14.1643 5.95378 14.0819L6.76975 12.1253C6.81217 11.9991 6.88828 11.9031 6.97347 11.8179C6.9933 11.818 7.01213 11.7992 7.022 11.7893L7.04979 11.7615C7.18251 11.6288 7.36896 11.548 7.58743 11.519C8.14278 11.4502 8.78656 11.7138 9.27215 12.1994C9.75773 12.685 10.0213 13.3288 9.94158 13.8731C9.90955 14.0527 9.85041 14.2375 9.75757 14.3742C9.72988 14.4219 9.69222 14.4595 9.65366 14.4981L9.64469 14.507ZM12.8686 5.94274C13.1752 5.63606 13.6458 5.57036 14.1274 5.71486C14.4066 5.79859 14.5232 6.1447 14.3322 6.3357L9.75179 10.9161C9.64239 11.0255 9.46488 11.0435 9.32122 10.9518C9.17277 10.859 9.01934 10.7791 8.86473 10.7123C8.61252 10.6036 8.52489 10.2864 8.70154 10.1098L12.8686 5.94274ZM15.5288 8.60299L11.3627 12.7691C11.1896 12.9422 10.8712 12.8677 10.7657 12.6223C10.6976 12.4591 10.6127 12.3007 10.5148 12.1472C10.4181 11.9967 10.4422 11.8091 10.556 11.6952L15.1257 7.1256C15.3149 6.93639 15.6609 7.05104 15.7465 7.33036C15.896 7.81701 15.8373 8.29452 15.5288 8.60299ZM17.3497 4.12182C16.1781 2.95023 14.4881 2.73979 13.5762 3.65174L6.22044 11.0075C6.0402 11.1877 5.8809 11.3889 5.78441 11.6349L4.23312 15.3599C4.03804 15.8302 4.18906 16.417 4.6218 16.8497C5.05454 17.2825 5.64137 17.4335 6.11165 17.2384L9.83661 15.6871C10.0817 15.5896 10.2828 15.4303 10.4631 15.2501L17.8188 7.89438C18.7308 6.98243 18.5213 5.29341 17.3497 4.12182Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.98116 1H15.0196C16.4042 1 17.6158 2.21154 17.6158 3.59615V10.8654H13.635C11.385 10.8654 9.65424 12.7692 9.65424 14.8462V18.8269H4.98116C3.59655 18.8269 2.38501 17.6154 2.38501 16.2308V3.59615C2.38501 2.21154 3.59655 1 4.98116 1ZM6.01963 9.82692H8.78886C9.48116 9.82692 10.0004 9.48077 10.0004 8.78846C10.0004 8.09615 9.48116 7.75 8.96193 7.75H6.01963C5.50039 7.75 4.98116 8.26923 4.98116 8.78846C4.98116 9.30769 5.50039 9.82692 6.01963 9.82692ZM14.1542 5.84615H6.01963C5.32732 5.84615 4.98116 5.32692 4.98116 4.80769C4.98116 4.28846 5.50039 3.76923 6.01963 3.76923H14.1542C14.6735 3.76923 15.1927 4.28846 15.1927 4.80769C15.0196 5.32692 14.6735 5.84615 14.1542 5.84615ZM17.4427 12.25H13.4619C12.0773 12.25 10.8658 13.4615 10.8658 15.0192V19L17.4427 12.25Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 927 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.27206 2.68377C8.40818 2.27543 8.79032 2 9.22075 2H10.7792C11.2097 2 11.5918 2.27543 11.7279 2.68377L12.3219 4.4658C12.7914 4.663 13.2304 4.91818 13.6302 5.22238L15.4721 4.84547C15.8938 4.75917 16.3234 4.9524 16.5386 5.32516L17.3178 6.67485C17.533 7.04761 17.4856 7.51627 17.2 7.83832L15.9529 9.24468C15.984 9.49208 16 9.74417 16 10C16 10.2558 15.984 10.5079 15.9529 10.7553L17.2 12.1617C17.4856 12.4837 17.533 12.9524 17.3178 13.3252L16.5386 14.6748C16.3234 15.0476 15.8938 15.2408 15.4721 15.1545L13.6302 14.7776C13.2304 15.0818 12.7914 15.337 12.3219 15.5342L11.7279 17.3162C11.5918 17.7246 11.2097 18 10.7792 18H9.22075C8.79032 18 8.40818 17.7246 8.27206 17.3162L7.67805 15.5342C7.20862 15.337 6.76955 15.0818 6.36982 14.7776L4.52792 15.1545C4.10623 15.2408 3.67663 15.0476 3.46141 14.6748L2.68217 13.3252C2.46696 12.9524 2.51442 12.4837 2.8 12.1617L4.04708 10.7553C4.016 10.5079 3.99999 10.2558 3.99999 10C3.99999 9.74416 4.016 9.49207 4.04707 9.24466L2.8 7.83832C2.51442 7.51627 2.46696 7.04761 2.68217 6.67485L3.46141 5.32516C3.67663 4.9524 4.10623 4.75917 4.52792 4.84547L6.3698 5.22238C6.76954 4.91818 7.20861 4.663 7.67805 4.4658L8.27206 2.68377ZM9.99999 8C8.89542 8 7.99999 8.89543 7.99999 10C7.99999 11.1046 8.89542 12 9.99999 12C11.1046 12 12 11.1046 12 10C12 8.89543 11.1046 8 9.99999 8Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,130 @@
|
||||
import classNames from 'classnames';
|
||||
import bindAll from 'lodash.bindall';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {FormattedMessage} 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 styles from './settings-menu.css';
|
||||
|
||||
import dropdownCaret from './dropdown-caret.svg';
|
||||
|
||||
class LanguageMenu extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'setRef',
|
||||
'handleMouseOver'
|
||||
]);
|
||||
}
|
||||
|
||||
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'});
|
||||
}
|
||||
}
|
||||
|
||||
setRef (component) {
|
||||
this.selectedRef = component;
|
||||
}
|
||||
|
||||
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'});
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<MenuItem
|
||||
expanded={this.props.menuOpen}
|
||||
>
|
||||
<div
|
||||
className={styles.option}
|
||||
onClick={this.props.onRequestOpen}
|
||||
onMouseOver={this.handleMouseOver}
|
||||
>
|
||||
<img
|
||||
className={styles.icon}
|
||||
src={languageIcon}
|
||||
/>
|
||||
<span className={styles.submenuLabel}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Language"
|
||||
description="Language sub-menu"
|
||||
id="gui.menuBar.language"
|
||||
/>
|
||||
</span>
|
||||
<img
|
||||
className={styles.expandCaret}
|
||||
src={dropdownCaret}
|
||||
/>
|
||||
</div>
|
||||
<Submenu
|
||||
className={styles.languageSubmenu}
|
||||
place={this.props.isRtl ? 'left' : 'right'}
|
||||
>
|
||||
{
|
||||
Object.keys(locales)
|
||||
.map(locale => (
|
||||
<MenuItem
|
||||
key={locale}
|
||||
className={styles.languageMenuItem}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => this.props.onChangeLanguage(locale)}
|
||||
>
|
||||
<img
|
||||
className={classNames(styles.check, {
|
||||
[styles.selected]: this.props.currentLocale === locale
|
||||
})}
|
||||
src={check}
|
||||
{...(this.props.currentLocale === locale && {ref: this.setRef})}
|
||||
/>
|
||||
{locales[locale].name}
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
</Submenu>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LanguageMenu.propTypes = {
|
||||
currentLocale: PropTypes.string,
|
||||
isRtl: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
menuOpen: PropTypes.bool,
|
||||
onChangeLanguage: PropTypes.func,
|
||||
onRequestCloseSettings: PropTypes.func,
|
||||
onRequestOpen: PropTypes.func
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
currentLocale: state.locales.locale,
|
||||
isRtl: state.locales.isRtl,
|
||||
menuOpen: languageMenuOpen(state),
|
||||
messagesByLocale: state.locales.messagesByLocale
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, ownProps) => ({
|
||||
onChangeLanguage: locale => {
|
||||
dispatch(selectLocale(locale));
|
||||
ownProps.onRequestCloseSettings();
|
||||
},
|
||||
onRequestOpen: () => dispatch(openLanguageMenu())
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(LanguageMenu);
|
||||
@@ -51,33 +51,6 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.language-icon {
|
||||
height: 1.5rem;
|
||||
vertical-align: middle;
|
||||
margin: 0 .125rem 0 0;
|
||||
}
|
||||
|
||||
[dir="rtl"] .language-icon {
|
||||
margin: 0 0 0 .125rem;
|
||||
}
|
||||
|
||||
.language-menu {
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.language-label {
|
||||
max-width: 7rem;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0 .5rem 0 .25rem;
|
||||
}
|
||||
|
||||
[dir="rtl"] .language-label {
|
||||
margin: 0 .25rem 0 .5rem;
|
||||
}
|
||||
|
||||
.menu-bar-item {
|
||||
display: flex;
|
||||
padding: 0 0.25rem;
|
||||
@@ -241,8 +214,16 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.collapsible-label {
|
||||
margin: 0 .5rem 0 .25rem;
|
||||
}
|
||||
|
||||
[dir="rtl"] .collapsible-label {
|
||||
margin: 0 0.25rem 0 0.5rem;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1024px) {
|
||||
.language-label, .tutorials-label {
|
||||
.tutorials-label, .collapsible-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import bowser from 'bowser';
|
||||
import React from 'react';
|
||||
|
||||
import VM from 'scratch-vm';
|
||||
import locales from 'scratch-l10n';
|
||||
|
||||
import Box from '../box/box.jsx';
|
||||
import Button from '../button/button.jsx';
|
||||
@@ -16,7 +15,6 @@ import CommunityButton from './community-button.jsx';
|
||||
import ShareButton from './share-button.jsx';
|
||||
import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
|
||||
import Divider from '../divider/divider.jsx';
|
||||
import LanguageSelector from '../../containers/language-selector.jsx';
|
||||
import SaveStatus from './save-status.jsx';
|
||||
import ProjectWatcher from '../../containers/project-watcher.jsx';
|
||||
import MenuBarMenu from './menu-bar-menu.jsx';
|
||||
@@ -29,7 +27,7 @@ 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 ThemeMenu from './theme-menu.jsx';
|
||||
import SettingsMenu from './settings-menu.jsx';
|
||||
|
||||
import {openTipsLibrary} from '../../reducers/modals';
|
||||
import {setPlayer} from '../../reducers/mode';
|
||||
@@ -63,20 +61,16 @@ import {
|
||||
openEditMenu,
|
||||
closeEditMenu,
|
||||
editMenuOpen,
|
||||
openLanguageMenu,
|
||||
closeLanguageMenu,
|
||||
languageMenuOpen,
|
||||
openLoginMenu,
|
||||
closeLoginMenu,
|
||||
loginMenuOpen,
|
||||
openThemeMenu,
|
||||
closeThemeMenu,
|
||||
themeMenuOpen,
|
||||
openModeMenu,
|
||||
closeModeMenu,
|
||||
modeMenuOpen
|
||||
modeMenuOpen,
|
||||
settingsMenuOpen,
|
||||
openSettingsMenu,
|
||||
closeSettingsMenu
|
||||
} from '../../reducers/menus';
|
||||
import {setTheme} from '../../reducers/theme.js';
|
||||
|
||||
import collectMetadata from '../../lib/collect-metadata';
|
||||
|
||||
@@ -87,8 +81,9 @@ 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 languageIcon from '../language-selector/language-icon.svg';
|
||||
import aboutIcon from './icon--about.svg';
|
||||
import fileIcon from './icon--file.svg';
|
||||
import editIcon from './icon--edit.svg';
|
||||
|
||||
import scratchLogo from './scratch-logo.svg';
|
||||
import ninetiesLogo from './nineties_logo.svg';
|
||||
@@ -97,14 +92,8 @@ import prehistoricLogo from './prehistoric-logo.svg';
|
||||
import oldtimeyLogo from './oldtimey-logo.svg';
|
||||
|
||||
import sharedMessages from '../../lib/shared-messages';
|
||||
import {persistTheme} from '../../lib/themes/themePersistance';
|
||||
|
||||
const ariaMessages = defineMessages({
|
||||
language: {
|
||||
id: 'gui.menuBar.LanguageSelector',
|
||||
defaultMessage: 'language selector',
|
||||
description: 'accessibility text for the language selection menu'
|
||||
},
|
||||
tutorials: {
|
||||
id: 'gui.menuBar.tutorialsLibrary',
|
||||
defaultMessage: 'Tutorials',
|
||||
@@ -183,7 +172,6 @@ class MenuBar extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleChangeTheme',
|
||||
'handleClickNew',
|
||||
'handleClickRemix',
|
||||
'handleClickSave',
|
||||
@@ -192,7 +180,6 @@ class MenuBar extends React.Component {
|
||||
'handleClickShare',
|
||||
'handleSetMode',
|
||||
'handleKeyPress',
|
||||
'handleLanguageMouseUp',
|
||||
'handleRestoreOption',
|
||||
'getSaveToComputerHandler',
|
||||
'restoreOptionMessage'
|
||||
@@ -305,16 +292,6 @@ class MenuBar extends React.Component {
|
||||
}
|
||||
};
|
||||
}
|
||||
handleLanguageMouseUp (e) {
|
||||
if (!this.props.languageMenuOpen) {
|
||||
this.props.onClickLanguage(e);
|
||||
}
|
||||
}
|
||||
handleChangeTheme (theme) {
|
||||
this.props.onChangeTheme(theme);
|
||||
this.props.onRequestCloseTheme();
|
||||
persistTheme(theme);
|
||||
}
|
||||
restoreOptionMessage (deletedItem) {
|
||||
switch (deletedItem) {
|
||||
case 'Sprite':
|
||||
@@ -459,29 +436,14 @@ class MenuBar extends React.Component {
|
||||
onClick={this.props.onClickLogo}
|
||||
/>
|
||||
</div>
|
||||
{(this.props.canChangeLanguage) && (<div
|
||||
className={classNames(styles.menuBarItem, styles.hoverable, styles.languageMenu)}
|
||||
>
|
||||
<img
|
||||
className={styles.languageIcon}
|
||||
src={languageIcon}
|
||||
/>
|
||||
<span className={styles.languageLabel}>{locales[this.props.currentLocale].name}</span>
|
||||
<img
|
||||
className={styles.languageCaret}
|
||||
src={dropdownCaret}
|
||||
/>
|
||||
<LanguageSelector label={this.props.intl.formatMessage(ariaMessages.language)} />
|
||||
</div>)}
|
||||
{(this.props.canChangeTheme) && (<ThemeMenu
|
||||
{(this.props.canChangeTheme || this.props.canChangeLanguage) && (<SettingsMenu
|
||||
canChangeLanguage={this.props.canChangeLanguage}
|
||||
canChangeTheme={this.props.canChangeTheme}
|
||||
isRtl={this.props.isRtl}
|
||||
onRequestClose={this.props.onRequestCloseTheme}
|
||||
onRequestOpen={this.props.onRequestOpenTheme}
|
||||
onChange={this.handleChangeTheme}
|
||||
theme={this.props.theme}
|
||||
themeMenuOpen={this.props.themeMenuOpen}
|
||||
onRequestClose={this.props.onRequestCloseSettings}
|
||||
onRequestOpen={this.props.onClickSettings}
|
||||
settingsMenuOpen={this.props.settingsMenuOpen}
|
||||
/>)}
|
||||
<Divider className={classNames(styles.divider)} />
|
||||
{(this.props.canManageFiles) && (
|
||||
<div
|
||||
className={classNames(styles.menuBarItem, styles.hoverable, {
|
||||
@@ -489,11 +451,15 @@ class MenuBar extends React.Component {
|
||||
})}
|
||||
onMouseUp={this.props.onClickFile}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="File"
|
||||
description="Text for file dropdown menu"
|
||||
id="gui.menuBar.file"
|
||||
/>
|
||||
<img src={fileIcon} />
|
||||
<span className={styles.collapsibleLabel}>
|
||||
<FormattedMessage
|
||||
defaultMessage="File"
|
||||
description="Text for file dropdown menu"
|
||||
id="gui.menuBar.file"
|
||||
/>
|
||||
</span>
|
||||
<img src={dropdownCaret} />
|
||||
<MenuBarMenu
|
||||
className={classNames(styles.menuBarMenu)}
|
||||
open={this.props.fileMenuOpen}
|
||||
@@ -555,13 +521,15 @@ class MenuBar extends React.Component {
|
||||
})}
|
||||
onMouseUp={this.props.onClickEdit}
|
||||
>
|
||||
<div className={classNames(styles.editMenu)}>
|
||||
<img src={editIcon} />
|
||||
<span className={styles.collapsibleLabel}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Edit"
|
||||
description="Text for edit dropdown menu"
|
||||
id="gui.menuBar.edit"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
<img src={dropdownCaret} />
|
||||
<MenuBarMenu
|
||||
className={classNames(styles.menuBarMenu)}
|
||||
open={this.props.editMenuOpen}
|
||||
@@ -900,17 +868,15 @@ MenuBar.propTypes = {
|
||||
isShowingProject: PropTypes.bool,
|
||||
isTotallyNormal: PropTypes.bool,
|
||||
isUpdating: PropTypes.bool,
|
||||
languageMenuOpen: PropTypes.bool,
|
||||
locale: PropTypes.string.isRequired,
|
||||
loginMenuOpen: PropTypes.bool,
|
||||
logo: PropTypes.string,
|
||||
onChangeTheme: PropTypes.func,
|
||||
modeMenuOpen: PropTypes.bool,
|
||||
modeNow: PropTypes.bool,
|
||||
mode220022BC: PropTypes.bool,
|
||||
mode1920: PropTypes.bool,
|
||||
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
|
||||
PropTypes.arrayOf( // menu mode: list of items in the About menu
|
||||
@@ -921,10 +887,8 @@ MenuBar.propTypes = {
|
||||
)
|
||||
]),
|
||||
onClickAccount: PropTypes.func,
|
||||
onSetTimeTravelMode: PropTypes.func,
|
||||
onClickEdit: PropTypes.func,
|
||||
onClickFile: PropTypes.func,
|
||||
onClickLanguage: PropTypes.func,
|
||||
onClickLogin: PropTypes.func,
|
||||
onClickLogo: PropTypes.func,
|
||||
onClickMode: PropTypes.func,
|
||||
@@ -932,33 +896,32 @@ MenuBar.propTypes = {
|
||||
onClickRemix: PropTypes.func,
|
||||
onClickSave: PropTypes.func,
|
||||
onClickSaveAsCopy: PropTypes.func,
|
||||
onClickSettings: PropTypes.func,
|
||||
onLogOut: PropTypes.func,
|
||||
onOpenRegistration: PropTypes.func,
|
||||
onOpenTipLibrary: PropTypes.func,
|
||||
onProjectTelemetryEvent: PropTypes.func,
|
||||
onRequestOpenAbout: PropTypes.func,
|
||||
onRequestCloseAbout: PropTypes.func,
|
||||
onRequestCloseAccount: PropTypes.func,
|
||||
onRequestCloseEdit: PropTypes.func,
|
||||
onRequestCloseFile: PropTypes.func,
|
||||
onRequestCloseLanguage: PropTypes.func,
|
||||
onRequestCloseLogin: PropTypes.func,
|
||||
onRequestCloseTheme: PropTypes.func,
|
||||
onRequestOpenTheme: PropTypes.func,
|
||||
onRequestCloseMode: PropTypes.func,
|
||||
onRequestCloseSettings: PropTypes.func,
|
||||
onRequestOpenAbout: PropTypes.func,
|
||||
onSeeCommunity: PropTypes.func,
|
||||
onSetTimeTravelMode: PropTypes.func,
|
||||
onShare: PropTypes.func,
|
||||
onStartSelectingFileUpload: PropTypes.func,
|
||||
onToggleLoginOpen: PropTypes.func,
|
||||
projectTitle: PropTypes.string,
|
||||
renderLogin: PropTypes.func,
|
||||
sessionExists: PropTypes.bool,
|
||||
settingsMenuOpen: PropTypes.bool,
|
||||
shouldSaveBeforeTransition: PropTypes.func,
|
||||
showComingSoon: PropTypes.bool,
|
||||
theme: PropTypes.string,
|
||||
themeMenuOpen: PropTypes.bool,
|
||||
userOwnsProject: PropTypes.bool,
|
||||
username: PropTypes.string,
|
||||
userOwnsProject: PropTypes.bool,
|
||||
vm: PropTypes.instanceOf(VM).isRequired
|
||||
};
|
||||
|
||||
@@ -979,14 +942,12 @@ const mapStateToProps = (state, ownProps) => {
|
||||
isRtl: state.locales.isRtl,
|
||||
isUpdating: getIsUpdating(loadingState),
|
||||
isShowingProject: getIsShowingProject(loadingState),
|
||||
languageMenuOpen: languageMenuOpen(state),
|
||||
locale: state.locales.locale,
|
||||
loginMenuOpen: loginMenuOpen(state),
|
||||
modeMenuOpen: modeMenuOpen(state),
|
||||
projectTitle: state.scratchGui.projectTitle,
|
||||
sessionExists: state.session && typeof state.session.session !== 'undefined',
|
||||
theme: state.scratchGui.theme.theme,
|
||||
themeMenuOpen: themeMenuOpen(state),
|
||||
settingsMenuOpen: settingsMenuOpen(state),
|
||||
username: user ? user.username : null,
|
||||
userOwnsProject: ownProps.authorUsername && user &&
|
||||
(ownProps.authorUsername === user.username),
|
||||
@@ -1008,22 +969,19 @@ const mapDispatchToProps = dispatch => ({
|
||||
onRequestCloseFile: () => dispatch(closeFileMenu()),
|
||||
onClickEdit: () => dispatch(openEditMenu()),
|
||||
onRequestCloseEdit: () => dispatch(closeEditMenu()),
|
||||
onClickLanguage: () => dispatch(openLanguageMenu()),
|
||||
onRequestCloseLanguage: () => dispatch(closeLanguageMenu()),
|
||||
onClickLogin: () => dispatch(openLoginMenu()),
|
||||
onRequestCloseLogin: () => dispatch(closeLoginMenu()),
|
||||
onClickMode: () => dispatch(openModeMenu()),
|
||||
onRequestCloseMode: () => dispatch(closeModeMenu()),
|
||||
onRequestOpenAbout: () => dispatch(openAboutMenu()),
|
||||
onRequestCloseAbout: () => dispatch(closeAboutMenu()),
|
||||
onRequestOpenTheme: () => dispatch(openThemeMenu()),
|
||||
onRequestCloseTheme: () => dispatch(closeThemeMenu()),
|
||||
onClickSettings: () => dispatch(openSettingsMenu()),
|
||||
onRequestCloseSettings: () => dispatch(closeSettingsMenu()),
|
||||
onClickNew: needSave => dispatch(requestNewProject(needSave)),
|
||||
onClickRemix: () => dispatch(remixProject()),
|
||||
onClickSave: () => dispatch(manualUpdateProject()),
|
||||
onClickSaveAsCopy: () => dispatch(saveProjectAsCopy()),
|
||||
onSeeCommunity: () => dispatch(setPlayer(true)),
|
||||
onChangeTheme: theme => dispatch(setTheme(theme)),
|
||||
onSetTimeTravelMode: mode => dispatch(setTimeTravel(mode))
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
.icon {
|
||||
width: 1.5rem;
|
||||
}
|
||||
|
||||
.theme-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.check {
|
||||
margin: 0 .25rem 0 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
[dir="rtl"] .check {
|
||||
margin: 0 0 0 .25rem;
|
||||
}
|
||||
|
||||
.selected {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.submenu-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dropdown-label {
|
||||
margin: 0 0.5rem 0 0.25rem;
|
||||
}
|
||||
|
||||
[dir="rtl"] .dropdown-label {
|
||||
margin: 0 0.25rem 0 0.5rem;
|
||||
}
|
||||
|
||||
.expand-caret {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
[dir="rtl"] .expand-caret {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* Extra padding to deal with scrollbar overlapping options in Firefox */
|
||||
|
||||
[dir="ltr"] .language-menu-item {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
[dir="rtl"] .language-menu-item {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.language-submenu > ul {
|
||||
max-height: calc(100vh - 5rem); /* Fallback if dvh not supported */
|
||||
max-height: calc(100dvh - 5rem);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1024px) {
|
||||
.dropdown-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
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 menuBarStyles from './menu-bar.css';
|
||||
import styles from './settings-menu.css';
|
||||
|
||||
import dropdownCaret from './dropdown-caret.svg';
|
||||
import settingsIcon from './icon--settings.svg';
|
||||
|
||||
const SettingsMenu = ({
|
||||
canChangeLanguage,
|
||||
canChangeTheme,
|
||||
isRtl,
|
||||
onRequestClose,
|
||||
onRequestOpen,
|
||||
settingsMenuOpen
|
||||
}) => (
|
||||
<div
|
||||
className={classNames(menuBarStyles.menuBarItem, menuBarStyles.hoverable, menuBarStyles.themeMenu, {
|
||||
[menuBarStyles.active]: settingsMenuOpen
|
||||
})}
|
||||
onMouseUp={onRequestOpen}
|
||||
>
|
||||
<img
|
||||
src={settingsIcon}
|
||||
/>
|
||||
<span className={styles.dropdownLabel}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Settings"
|
||||
description="Settings menu"
|
||||
id="gui.menuBar.settings"
|
||||
/>
|
||||
</span>
|
||||
<img src={dropdownCaret} />
|
||||
<MenuBarMenu
|
||||
className={menuBarStyles.menuBarMenu}
|
||||
open={settingsMenuOpen}
|
||||
place={isRtl ? 'left' : 'right'}
|
||||
onRequestClose={onRequestClose}
|
||||
>
|
||||
<MenuSection>
|
||||
{canChangeLanguage && <LanguageMenu onRequestCloseSettings={onRequestClose} />}
|
||||
{canChangeTheme && <ThemeMenu onRequestCloseSettings={onRequestClose} />}
|
||||
</MenuSection>
|
||||
</MenuBarMenu>
|
||||
</div>
|
||||
);
|
||||
|
||||
SettingsMenu.propTypes = {
|
||||
canChangeLanguage: PropTypes.bool,
|
||||
canChangeTheme: PropTypes.bool,
|
||||
isRtl: PropTypes.bool,
|
||||
onRequestClose: PropTypes.func,
|
||||
onRequestOpen: PropTypes.func,
|
||||
settingsMenuOpen: PropTypes.bool
|
||||
};
|
||||
|
||||
export default SettingsMenu;
|
||||
@@ -1,52 +0,0 @@
|
||||
@import "../../css/colors.css";
|
||||
|
||||
.theme-label {
|
||||
max-width: 7rem;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0 .5rem 0 .25rem;
|
||||
}
|
||||
|
||||
[dir="rtl"] .theme-label {
|
||||
margin: 0 .25rem 0 .5rem;
|
||||
}
|
||||
|
||||
.theme-icon {
|
||||
height: 1.5rem;
|
||||
vertical-align: middle;
|
||||
margin: 0 .25rem 0 0;
|
||||
}
|
||||
|
||||
[dir="rtl"] .theme-icon {
|
||||
margin: 0 0 0 .25rem;
|
||||
}
|
||||
|
||||
.theme-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.theme-active {
|
||||
background-color: $ui-black-transparent;
|
||||
}
|
||||
|
||||
.check {
|
||||
margin: 0 .25rem 0 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
[dir="rtl"] .check {
|
||||
margin: 0 0 0 .25rem;
|
||||
}
|
||||
|
||||
.selected {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1024px) {
|
||||
.theme-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,105 +1,118 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {defineMessages, FormattedMessage, injectIntl, intlShape} from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import MenuBarMenu from './menu-bar-menu.jsx';
|
||||
import check from './check.svg';
|
||||
import {MenuItem, Submenu} from '../menu/menu.jsx';
|
||||
import {DEFAULT_THEME, HIGH_CONTRAST_THEME, themeMap} from '../../lib/themes';
|
||||
import {MenuItem, MenuSection} from '../menu/menu.jsx';
|
||||
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';
|
||||
import check from './check.svg';
|
||||
|
||||
import styles from './theme-menu.css';
|
||||
import menuBarStyles from './menu-bar.css';
|
||||
|
||||
const messages = defineMessages({
|
||||
colorTheme: {
|
||||
id: 'gui.theme.colorTheme',
|
||||
defaultMessage: 'Color theme',
|
||||
description: 'label for selecting the site\'s color theme'
|
||||
}
|
||||
});
|
||||
|
||||
const ThemeMenuItem = props => {
|
||||
const themeInfo = themeMap[props.theme];
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
isRtl={props.isRtl}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<div className={styles.themeOption}>
|
||||
<MenuItem onClick={props.onClick}>
|
||||
<div className={styles.option}>
|
||||
<img
|
||||
src={check}
|
||||
className={classNames(styles.check, {[styles.selected]: props.isSelected})}
|
||||
src={check}
|
||||
/>
|
||||
<img
|
||||
className={styles.icon}
|
||||
src={themeInfo.icon}
|
||||
/>
|
||||
<img src={themeInfo.icon} />
|
||||
<FormattedMessage {...themeInfo.label} />
|
||||
</div>
|
||||
</MenuItem>);
|
||||
};
|
||||
|
||||
ThemeMenuItem.propTypes = {
|
||||
isRtl: PropTypes.bool,
|
||||
isSelected: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
const ThemeMenu = props => {
|
||||
const ThemeMenu = ({
|
||||
isRtl,
|
||||
menuOpen,
|
||||
onChangeTheme,
|
||||
onRequestOpen,
|
||||
theme
|
||||
}) => {
|
||||
const enabledThemes = [DEFAULT_THEME, HIGH_CONTRAST_THEME];
|
||||
const themeInfo = themeMap[props.theme];
|
||||
const themeInfo = themeMap[theme];
|
||||
|
||||
return (<div
|
||||
className={classNames(menuBarStyles.menuBarItem, menuBarStyles.hoverable, menuBarStyles.themeMenu, {
|
||||
[menuBarStyles.active]: props.themeMenuOpen
|
||||
})}
|
||||
onMouseUp={props.onRequestOpen}
|
||||
>
|
||||
<img
|
||||
alt={props.intl.formatMessage(messages.colorTheme)}
|
||||
className={styles.themeIcon}
|
||||
src={themeInfo.icon}
|
||||
/>
|
||||
<span className={styles.themeLabel}>
|
||||
<FormattedMessage {...themeInfo.label} />
|
||||
</span>
|
||||
<img
|
||||
className={styles.themeCaret}
|
||||
src={dropdownCaret}
|
||||
/>
|
||||
<MenuBarMenu
|
||||
className={menuBarStyles.menuBarMenu}
|
||||
open={props.themeMenuOpen}
|
||||
place={props.isRtl ? 'left' : 'right'}
|
||||
onRequestClose={props.onRequestClose}
|
||||
>
|
||||
<MenuSection>
|
||||
return (
|
||||
<MenuItem expanded={menuOpen}>
|
||||
<div
|
||||
className={styles.option}
|
||||
onClick={onRequestOpen}
|
||||
>
|
||||
<img
|
||||
src={themeInfo.icon}
|
||||
style={{width: 24}}
|
||||
/>
|
||||
<span className={styles.submenuLabel}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Color Mode"
|
||||
description="Color mode sub-menu"
|
||||
id="gui.menuBar.colorMode"
|
||||
/>
|
||||
</span>
|
||||
<img
|
||||
className={styles.expandCaret}
|
||||
src={dropdownCaret}
|
||||
/>
|
||||
</div>
|
||||
<Submenu place={isRtl ? 'left' : 'right'}>
|
||||
{enabledThemes.map(enabledTheme => (
|
||||
<ThemeMenuItem
|
||||
key={enabledTheme}
|
||||
isRtl={props.isRtl}
|
||||
isSelected={props.theme === enabledTheme}
|
||||
isSelected={theme === enabledTheme}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => props.onChange(enabledTheme)}
|
||||
onClick={() => onChangeTheme(enabledTheme)}
|
||||
theme={enabledTheme}
|
||||
/>)
|
||||
)}
|
||||
</MenuSection>
|
||||
</MenuBarMenu>
|
||||
</div>);
|
||||
</Submenu>
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
ThemeMenu.propTypes = {
|
||||
className: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
isRtl: PropTypes.bool,
|
||||
onChange: PropTypes.func,
|
||||
onRequestClose: PropTypes.func,
|
||||
menuOpen: PropTypes.bool,
|
||||
onChangeTheme: PropTypes.func,
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
onRequestCloseSettings: PropTypes.func,
|
||||
onRequestOpen: PropTypes.func,
|
||||
theme: PropTypes.string,
|
||||
themeMenuOpen: PropTypes.bool
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
export default injectIntl(ThemeMenu);
|
||||
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);
|
||||
@@ -30,10 +30,12 @@
|
||||
font-size: .75rem;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menu-item.active,
|
||||
.menu-item:hover {
|
||||
.menu-item:hover,
|
||||
.menu-item.expanded {
|
||||
background-color: $ui-black-transparent;
|
||||
}
|
||||
|
||||
@@ -44,3 +46,54 @@
|
||||
.menu-section {
|
||||
border-top: 1px solid $ui-black-transparent;
|
||||
}
|
||||
|
||||
.submenu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
min-width: 186px;
|
||||
max-width: 260px;
|
||||
height: max-content;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.submenu.right {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.submenu.left {
|
||||
right: 100%;
|
||||
}
|
||||
|
||||
.submenu > .menu {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-color: $ui-black-transparent $looks-secondary;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.menu-item.expanded > .submenu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
:not(:has(> .menu-item.expanded)) > .menu-item:hover > .submenu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Fallback styling for webkit browsers that don't support scrollbar-color */
|
||||
|
||||
.submenu > .menu::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.submenu > .menu::-webkit-scrollbar-track {
|
||||
background: $looks-secondary;
|
||||
}
|
||||
|
||||
.submenu > .menu::-webkit-scrollbar-thumb {
|
||||
background-color: $ui-black-transparent;
|
||||
border-radius: 12px;
|
||||
border: 3px solid $looks-secondary;
|
||||
}
|
||||
@@ -33,16 +33,44 @@ MenuComponent.propTypes = {
|
||||
};
|
||||
|
||||
|
||||
const Submenu = ({children, className, place, ...props}) => (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.submenu,
|
||||
className,
|
||||
{
|
||||
[styles.left]: place === 'left',
|
||||
[styles.right]: place === 'right'
|
||||
}
|
||||
)}
|
||||
>
|
||||
<MenuComponent
|
||||
place={place}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MenuComponent>
|
||||
</div>
|
||||
);
|
||||
|
||||
Submenu.propTypes = {
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
place: PropTypes.oneOf(['left', 'right'])
|
||||
};
|
||||
|
||||
const MenuItem = ({
|
||||
children,
|
||||
className,
|
||||
expanded = false,
|
||||
onClick
|
||||
}) => (
|
||||
<li
|
||||
className={classNames(
|
||||
styles.menuItem,
|
||||
styles.hoverable,
|
||||
className
|
||||
className,
|
||||
{[styles.expanded]: expanded}
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
@@ -53,6 +81,7 @@ const MenuItem = ({
|
||||
MenuItem.propTypes = {
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
expanded: PropTypes.bool,
|
||||
onClick: PropTypes.func
|
||||
};
|
||||
|
||||
@@ -80,5 +109,6 @@ MenuSection.propTypes = {
|
||||
export {
|
||||
MenuComponent as default,
|
||||
MenuItem,
|
||||
MenuSection
|
||||
MenuSection,
|
||||
Submenu
|
||||
};
|
||||
+104
-26
@@ -3,36 +3,99 @@ const CLOSE_MENU = 'scratch-gui/menus/CLOSE_MENU';
|
||||
|
||||
const MENU_ABOUT = 'aboutMenu';
|
||||
const MENU_ACCOUNT = 'accountMenu';
|
||||
const MENU_FILE = 'fileMenu';
|
||||
const MENU_EDIT = 'editMenu';
|
||||
const MENU_FILE = 'fileMenu';
|
||||
const MENU_LANGUAGE = 'languageMenu';
|
||||
const MENU_LOGIN = 'loginMenu';
|
||||
const MENU_THEME = 'themeMenu';
|
||||
const MENU_MODE = 'modeMenu';
|
||||
const MENU_SETTINGS = 'settingsMenu';
|
||||
const MENU_THEME = 'themeMenu';
|
||||
|
||||
class Menu {
|
||||
constructor (id) {
|
||||
this.id = id;
|
||||
this.children = [];
|
||||
this.parent = null;
|
||||
}
|
||||
|
||||
addChild (menu) {
|
||||
this.children.push(menu);
|
||||
menu.parent = this;
|
||||
return this;
|
||||
}
|
||||
|
||||
descendants () {
|
||||
return this.children.flatMap(child => [child, ...child.descendants()]);
|
||||
}
|
||||
|
||||
siblings () {
|
||||
if (!this.parent) return [];
|
||||
|
||||
return this.parent.children.filter(child => child.id !== this.id);
|
||||
}
|
||||
|
||||
findById (id) {
|
||||
if (this.id === id) return this;
|
||||
|
||||
for (const child of this.children) {
|
||||
const found = child.findById(id);
|
||||
if (found) return found;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 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_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_ABOUT));
|
||||
|
||||
const initialState = {
|
||||
[MENU_ABOUT]: false,
|
||||
[MENU_ACCOUNT]: false,
|
||||
[MENU_FILE]: false,
|
||||
[MENU_EDIT]: false,
|
||||
[MENU_FILE]: false,
|
||||
[MENU_LANGUAGE]: false,
|
||||
[MENU_LOGIN]: false,
|
||||
[MENU_THEME]: false,
|
||||
[MENU_MODE]: false
|
||||
[MENU_MODE]: false,
|
||||
[MENU_SETTINGS]: false,
|
||||
[MENU_THEME]: false
|
||||
};
|
||||
|
||||
const reducer = function (state, action) {
|
||||
if (typeof state === 'undefined') state = initialState;
|
||||
switch (action.type) {
|
||||
case OPEN_MENU:
|
||||
return Object.assign({}, state, {
|
||||
[action.menu]: true
|
||||
});
|
||||
case CLOSE_MENU:
|
||||
return Object.assign({}, state, {
|
||||
[action.menu]: false
|
||||
});
|
||||
case OPEN_MENU: {
|
||||
const menu = rootMenu.findById(action.menu);
|
||||
// Close siblings when opening a menu
|
||||
const toClose = menu.siblings().flatMap(sibling => [sibling, ...sibling.descendants()]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
[action.menu]: true,
|
||||
...Object.fromEntries(toClose.map(({id}) => [id, false]))
|
||||
};
|
||||
}
|
||||
case CLOSE_MENU: {
|
||||
const menu = rootMenu.findById(action.menu);
|
||||
// Close this menu and any submenus
|
||||
const toClose = [menu, ...menu.descendants()];
|
||||
|
||||
return {
|
||||
...state,
|
||||
...Object.fromEntries(toClose.map(({id}) => [id, false]))
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -45,31 +108,43 @@ const closeMenu = menu => ({
|
||||
type: CLOSE_MENU,
|
||||
menu: menu
|
||||
});
|
||||
|
||||
const openAboutMenu = () => openMenu(MENU_ABOUT);
|
||||
const closeAboutMenu = () => closeMenu(MENU_ABOUT);
|
||||
const aboutMenuOpen = state => state.scratchGui.menus[MENU_ABOUT];
|
||||
|
||||
const openAccountMenu = () => openMenu(MENU_ACCOUNT);
|
||||
const closeAccountMenu = () => closeMenu(MENU_ACCOUNT);
|
||||
const accountMenuOpen = state => state.scratchGui.menus[MENU_ACCOUNT];
|
||||
const openFileMenu = () => openMenu(MENU_FILE);
|
||||
const closeFileMenu = () => closeMenu(MENU_FILE);
|
||||
const fileMenuOpen = state => state.scratchGui.menus[MENU_FILE];
|
||||
|
||||
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);
|
||||
const loginMenuOpen = state => state.scratchGui.menus[MENU_LOGIN];
|
||||
const openThemeMenu = () => openMenu(MENU_THEME);
|
||||
const closeThemeMenu = () => closeMenu(MENU_THEME);
|
||||
const themeMenuOpen = state => state.scratchGui.menus[MENU_THEME];
|
||||
|
||||
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,
|
||||
initialState as menuInitialState,
|
||||
@@ -79,22 +154,25 @@ export {
|
||||
openAccountMenu,
|
||||
closeAccountMenu,
|
||||
accountMenuOpen,
|
||||
openFileMenu,
|
||||
closeFileMenu,
|
||||
fileMenuOpen,
|
||||
openEditMenu,
|
||||
closeEditMenu,
|
||||
editMenuOpen,
|
||||
openFileMenu,
|
||||
closeFileMenu,
|
||||
fileMenuOpen,
|
||||
openLanguageMenu,
|
||||
closeLanguageMenu,
|
||||
languageMenuOpen,
|
||||
openLoginMenu,
|
||||
closeLoginMenu,
|
||||
loginMenuOpen,
|
||||
openThemeMenu,
|
||||
closeThemeMenu,
|
||||
themeMenuOpen,
|
||||
openModeMenu,
|
||||
closeModeMenu,
|
||||
modeMenuOpen
|
||||
modeMenuOpen,
|
||||
openSettingsMenu,
|
||||
closeSettingsMenu,
|
||||
settingsMenuOpen,
|
||||
openThemeMenu,
|
||||
closeThemeMenu,
|
||||
themeMenuOpen
|
||||
};
|
||||
@@ -21,6 +21,9 @@ const uri = path.resolve(__dirname, '../../build/index.html');
|
||||
|
||||
let driver;
|
||||
|
||||
const SETTINGS_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' +
|
||||
'[*[contains(@class, "settings-menu_dropdown-label")]//*[text()="Settings"]]';
|
||||
|
||||
describe('Working with the blocks', () => {
|
||||
beforeAll(() => {
|
||||
driver = getDriver();
|
||||
@@ -309,7 +312,8 @@ describe('Working with the blocks', () => {
|
||||
await findByText('1', scope.reportedValue);
|
||||
|
||||
// change language
|
||||
await clickXpath('//*[@aria-label="language selector"]');
|
||||
await clickXpath(SETTINGS_MENU_XPATH);
|
||||
await clickText('Language', scope.menuBar);
|
||||
await clickText('Deutsch');
|
||||
|
||||
await clickText('Skripte');
|
||||
|
||||
@@ -17,6 +17,11 @@ const uri = path.resolve(__dirname, '../../build/index.html');
|
||||
|
||||
let driver;
|
||||
|
||||
const FILE_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' +
|
||||
'[*[contains(@class, "menu-bar_collapsible-label")]//*[text()="File"]]';
|
||||
const SETTINGS_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' +
|
||||
'[*[contains(@class, "settings-menu_dropdown-label")]//*[text()="Settings"]]';
|
||||
|
||||
describe('Localization', () => {
|
||||
beforeAll(() => {
|
||||
driver = getDriver();
|
||||
@@ -33,7 +38,8 @@ describe('Localization', () => {
|
||||
await clickXpath('//button[@aria-label="Choose a Sprite"]');
|
||||
await clickText('Apple', scope.modal); // Closes modal
|
||||
|
||||
await clickXpath('//*[@aria-label="language selector"]');
|
||||
await clickXpath(SETTINGS_MENU_XPATH);
|
||||
await clickText('Language', scope.menuBar);
|
||||
await clickText('Deutsch');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // wait for blocks refresh
|
||||
|
||||
@@ -79,7 +85,7 @@ describe('Localization', () => {
|
||||
// Regression test for ENA-142, monitor can lag behind language selection
|
||||
test('Monitor labels update on locale change', async () => {
|
||||
await loadUri(uri);
|
||||
await clickText('File');
|
||||
await clickXpath(FILE_MENU_XPATH);
|
||||
await clickText('Load from your computer');
|
||||
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
|
||||
await input.sendKeys(path.resolve(__dirname, '../fixtures/monitor-variable.sb3'));
|
||||
@@ -89,7 +95,8 @@ describe('Localization', () => {
|
||||
await findByText('language', scope.monitors);
|
||||
|
||||
// Change locale to ja
|
||||
await clickXpath('//*[@aria-label="language selector"]');
|
||||
await clickXpath(SETTINGS_MENU_XPATH);
|
||||
await clickText('Language', scope.menuBar);
|
||||
await clickText('日本語');
|
||||
|
||||
// Monitor labels updated
|
||||
@@ -97,6 +104,8 @@ describe('Localization', () => {
|
||||
await findByText('言語', scope.monitors);
|
||||
|
||||
// Change locale to ja-hira
|
||||
await clickXpath(SETTINGS_MENU_XPATH);
|
||||
await clickText('Language', scope.menuBar);
|
||||
await clickText('にほんご');
|
||||
|
||||
// Monitor labels updated
|
||||
|
||||
@@ -16,6 +16,11 @@ const uri = path.resolve(__dirname, '../../build/index.html');
|
||||
|
||||
let driver;
|
||||
|
||||
const FILE_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' +
|
||||
'[*[contains(@class, "menu-bar_collapsible-label")]//*[text()="File"]]';
|
||||
const SETTINGS_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' +
|
||||
'[*[contains(@class, "settings-menu_dropdown-label")]//*[text()="Settings"]]';
|
||||
|
||||
describe('Menu bar settings', () => {
|
||||
beforeAll(() => {
|
||||
driver = getDriver();
|
||||
@@ -27,28 +32,19 @@ describe('Menu bar settings', () => {
|
||||
|
||||
test('File->New should be enabled', async () => {
|
||||
await loadUri(uri);
|
||||
await clickXpath(
|
||||
'//div[contains(@class, "menu-bar_menu-bar-item") and ' +
|
||||
'contains(@class, "menu-bar_hoverable")][span[text()="File"]]'
|
||||
);
|
||||
await clickXpath(FILE_MENU_XPATH);
|
||||
await findByXpath('//*[li[span[text()="New"]] and not(@data-tip="tooltip")]');
|
||||
});
|
||||
|
||||
test('File->Load should be enabled', async () => {
|
||||
await loadUri(uri);
|
||||
await clickXpath(
|
||||
'//div[contains(@class, "menu-bar_menu-bar-item") and ' +
|
||||
'contains(@class, "menu-bar_hoverable")][span[text()="File"]]'
|
||||
);
|
||||
await clickXpath(FILE_MENU_XPATH);
|
||||
await findByXpath('//*[li[text()="Load from your computer"] and not(@data-tip="tooltip")]');
|
||||
});
|
||||
|
||||
test('File->Save should be enabled', async () => {
|
||||
await loadUri(uri);
|
||||
await clickXpath(
|
||||
'//div[contains(@class, "menu-bar_menu-bar-item") and ' +
|
||||
'contains(@class, "menu-bar_hoverable")][span[text()="File"]]'
|
||||
);
|
||||
await clickXpath(FILE_MENU_XPATH);
|
||||
await findByXpath('//*[li[span[text()="Save to your computer"]] and not(@data-tip="tooltip")]');
|
||||
});
|
||||
|
||||
@@ -74,7 +70,7 @@ describe('Menu bar settings', () => {
|
||||
|
||||
test('User is not warned before uploading project file over a fresh project', async () => {
|
||||
await loadUri(uri);
|
||||
await clickText('File');
|
||||
await clickXpath(FILE_MENU_XPATH);
|
||||
await clickText('Load from your computer');
|
||||
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
|
||||
await input.sendKeys(path.resolve(__dirname, '../fixtures/project1.sb3'));
|
||||
@@ -89,7 +85,7 @@ describe('Menu bar settings', () => {
|
||||
await rightClickText('Sprite1', scope.spriteTile);
|
||||
await clickText('delete', scope.spriteTile);
|
||||
|
||||
await clickText('File');
|
||||
await clickXpath(FILE_MENU_XPATH);
|
||||
await clickText('Load from your computer');
|
||||
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
|
||||
await input.sendKeys(path.resolve(__dirname, '../fixtures/project1.sb3'));
|
||||
@@ -100,15 +96,17 @@ describe('Menu bar settings', () => {
|
||||
|
||||
test('Theme picker shows themes', async () => {
|
||||
await loadUri(uri);
|
||||
await clickXpath('//img[@alt="Color theme"]');
|
||||
await clickXpath(SETTINGS_MENU_XPATH);
|
||||
await clickText('Color Mode', scope.menuBar);
|
||||
|
||||
await findByText('Original', scope.menuBar);
|
||||
await findByText('High Contrast', scope.menuBar);
|
||||
expect(await (await findByText('Original', scope.menuBar)).isDisplayed()).toBe(true);
|
||||
expect(await (await findByText('High Contrast', scope.menuBar)).isDisplayed()).toBe(true);
|
||||
});
|
||||
|
||||
test('Theme picker switches to high contrast', async () => {
|
||||
await loadUri(uri);
|
||||
await clickXpath('//img[@alt="Color theme"]');
|
||||
await clickXpath(SETTINGS_MENU_XPATH);
|
||||
await clickText('Color Mode', scope.menuBar);
|
||||
await clickText('High Contrast', scope.menuBar);
|
||||
|
||||
// There is a tiny delay for the color theme to be applied to the categories.
|
||||
@@ -124,4 +122,51 @@ describe('Menu bar settings', () => {
|
||||
return /128,\s?181,\s?255/.test(color) || color.includes('80B5FF');
|
||||
}, 5000, 'Motion category color does not match high contrast theme');
|
||||
});
|
||||
|
||||
test('Settings menu switches between submenus', async () => {
|
||||
await loadUri(uri);
|
||||
await clickXpath(SETTINGS_MENU_XPATH);
|
||||
|
||||
// Language and theme options not visible yet
|
||||
expect(await (await findByText('High Contrast', scope.menuBar)).isDisplayed()).toBe(false);
|
||||
expect(await (await findByText('Esperanto', scope.menuBar)).isDisplayed()).toBe(false);
|
||||
|
||||
await clickText('Color Mode', scope.menuBar);
|
||||
|
||||
// Only theme options visible
|
||||
expect(await (await findByText('High Contrast', scope.menuBar)).isDisplayed()).toBe(true);
|
||||
expect(await (await findByText('Esperanto', scope.menuBar)).isDisplayed()).toBe(false);
|
||||
|
||||
await clickText('Language', scope.menuBar);
|
||||
|
||||
// Only language options visible
|
||||
expect(await (await findByText('High Contrast', scope.menuBar)).isDisplayed()).toBe(false);
|
||||
expect(await (await findByText('Esperanto', scope.menuBar)).isDisplayed()).toBe(true);
|
||||
});
|
||||
|
||||
test('Menu labels hidden when width is equal to 1024', async () => {
|
||||
await loadUri(uri);
|
||||
await driver.manage()
|
||||
.window()
|
||||
.setSize(1024, 768);
|
||||
|
||||
const collapsibleMenus = ['Settings', 'File', 'Edit', 'Tutorials'];
|
||||
for (const menu of collapsibleMenus) {
|
||||
const settingsMenu = await findByText(menu, scope.menuBar);
|
||||
expect(await settingsMenu.isDisplayed()).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test('Menu labels shown when width is greater than 1024', async () => {
|
||||
await loadUri(uri);
|
||||
await driver.manage()
|
||||
.window()
|
||||
.setSize(1200, 768);
|
||||
|
||||
const collapsibleMenus = ['Settings', 'File', 'Edit', 'Tutorials'];
|
||||
for (const menu of collapsibleMenus) {
|
||||
const settingsMenu = await findByText(menu, scope.menuBar);
|
||||
expect(await settingsMenu.isDisplayed()).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,9 @@ const uri = path.resolve(__dirname, '../../build/index.html');
|
||||
|
||||
let driver;
|
||||
|
||||
const FILE_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' +
|
||||
'[*[contains(@class, "menu-bar_collapsible-label")]//*[text()="File"]]';
|
||||
|
||||
describe('Loading scratch gui', () => {
|
||||
beforeAll(() => {
|
||||
driver = getDriver();
|
||||
@@ -82,10 +85,7 @@ describe('Loading scratch gui', () => {
|
||||
await loadUri(uri);
|
||||
await findByXpath('//*[span[text()="Costumes"]]');
|
||||
await clickText('Costumes');
|
||||
await clickXpath(
|
||||
'//div[contains(@class, "menu-bar_menu-bar-item") and ' +
|
||||
'contains(@class, "menu-bar_hoverable")][span[text()="File"]]'
|
||||
);
|
||||
await clickXpath(FILE_MENU_XPATH);
|
||||
await clickXpath('//li[span[text()="New"]]');
|
||||
await findByXpath('//div[@class="scratchCategoryMenu"]');
|
||||
await clickText('Operators', scope.blocksTab);
|
||||
@@ -93,10 +93,7 @@ describe('Loading scratch gui', () => {
|
||||
|
||||
test('Not logged in->made no changes to project->create new project should not show alert', async () => {
|
||||
await loadUri(uri);
|
||||
await clickXpath(
|
||||
'//div[contains(@class, "menu-bar_menu-bar-item") and ' +
|
||||
'contains(@class, "menu-bar_hoverable")][span[text()="File"]]'
|
||||
);
|
||||
await clickXpath(FILE_MENU_XPATH);
|
||||
await clickXpath('//li[span[text()="New"]]');
|
||||
await findByXpath('//*[div[@class="scratchCategoryMenu"]]');
|
||||
await clickText('Operators', scope.blocksTab);
|
||||
@@ -108,10 +105,7 @@ describe('Loading scratch gui', () => {
|
||||
await clickXpath('//button[@aria-label="Choose a Sound"]');
|
||||
await clickText('A Bass', scope.modal); // Should close the modal
|
||||
await findByText('1.28'); // length of A Bass sound
|
||||
await clickXpath(
|
||||
'//div[contains(@class, "menu-bar_menu-bar-item") and ' +
|
||||
'contains(@class, "menu-bar_hoverable")][span[text()="File"]]'
|
||||
);
|
||||
await clickXpath(FILE_MENU_XPATH);
|
||||
await clickXpath('//li[span[text()="New"]]');
|
||||
driver.switchTo()
|
||||
.alert()
|
||||
|
||||
@@ -14,6 +14,9 @@ const uri = path.resolve(__dirname, '../../build/index.html');
|
||||
|
||||
let driver;
|
||||
|
||||
const FILE_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' +
|
||||
'[*[contains(@class, "menu-bar_collapsible-label")]//*[text()="File"]]';
|
||||
|
||||
describe('Project state', () => {
|
||||
beforeAll(() => {
|
||||
driver = getDriver();
|
||||
@@ -34,10 +37,7 @@ describe('Project state', () => {
|
||||
await clickText('Costumes'); // just to blur the input
|
||||
// verify that project title has changed
|
||||
await clickXpath('//input[@value="Changed title of project"]');
|
||||
await clickXpath(
|
||||
'//div[contains(@class, "menu-bar_menu-bar-item") and ' +
|
||||
'contains(@class, "menu-bar_hoverable")][span[text()="File"]]'
|
||||
);
|
||||
await clickXpath(FILE_MENU_XPATH);
|
||||
await clickXpath('//li[span[text()="New"]]');
|
||||
// project title should be default again
|
||||
await clickXpath(`//input[@value="${defaultProjectTitle}"]`);
|
||||
|
||||
@@ -14,6 +14,9 @@ const uri = path.resolve(__dirname, '../../build/index.html');
|
||||
|
||||
let driver;
|
||||
|
||||
const FILE_MENU_XPATH = '//div[contains(@class, "menu-bar_menu-bar-item")]' +
|
||||
'[*[contains(@class, "menu-bar_collapsible-label")]//*[text()="File"]]';
|
||||
|
||||
describe('Loading scratch gui', () => {
|
||||
beforeAll(() => {
|
||||
driver = getDriver();
|
||||
@@ -25,7 +28,7 @@ describe('Loading scratch gui', () => {
|
||||
|
||||
test('Loading project file from computer succeeds, without opening failure alert', async () => {
|
||||
await loadUri(uri);
|
||||
await clickText('File');
|
||||
await clickXpath(FILE_MENU_XPATH);
|
||||
await clickText('Load from your computer');
|
||||
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
|
||||
await input.sendKeys(path.resolve(__dirname, '../fixtures/project1.sb3'));
|
||||
@@ -35,7 +38,7 @@ describe('Loading scratch gui', () => {
|
||||
|
||||
test('Loading project file from computer gives project the filename from file', async () => {
|
||||
await loadUri(uri);
|
||||
await clickText('File');
|
||||
await clickXpath(FILE_MENU_XPATH);
|
||||
await clickText('Load from your computer');
|
||||
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
|
||||
await input.sendKeys(path.resolve(__dirname, '../fixtures/project1.sb3'));
|
||||
@@ -45,7 +48,7 @@ describe('Loading scratch gui', () => {
|
||||
|
||||
test('Load sb3 project with a missing svg costume', async () => {
|
||||
await loadUri(uri);
|
||||
await clickText('File');
|
||||
await clickXpath(FILE_MENU_XPATH);
|
||||
await clickText('Load from your computer');
|
||||
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
|
||||
await input.sendKeys(path.resolve(__dirname, '../fixtures/missing-sprite-svg.sb3'));
|
||||
@@ -56,7 +59,7 @@ describe('Loading scratch gui', () => {
|
||||
|
||||
test('Load sb3 project with an invalid svg costume', async () => {
|
||||
await loadUri(uri);
|
||||
await clickText('File');
|
||||
await clickXpath(FILE_MENU_XPATH);
|
||||
await clickText('Load from your computer');
|
||||
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
|
||||
await input.sendKeys(path.resolve(__dirname, '../fixtures/corrupt-svg.sb3'));
|
||||
@@ -67,7 +70,7 @@ describe('Loading scratch gui', () => {
|
||||
|
||||
test('Load sb2 project with a missing svg costume', async () => {
|
||||
await loadUri(uri);
|
||||
await clickText('File');
|
||||
await clickXpath(FILE_MENU_XPATH);
|
||||
await clickText('Load from your computer');
|
||||
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
|
||||
await input.sendKeys(path.resolve(__dirname, '../fixtures/missing-svg.sb2'));
|
||||
@@ -78,7 +81,7 @@ describe('Loading scratch gui', () => {
|
||||
|
||||
test('Load sb2 project with an invalid svg costume', async () => {
|
||||
await loadUri(uri);
|
||||
await clickText('File');
|
||||
await clickXpath(FILE_MENU_XPATH);
|
||||
await clickText('Load from your computer');
|
||||
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
|
||||
await input.sendKeys(path.resolve(__dirname, '../fixtures/corrupt-svg.sb2'));
|
||||
@@ -89,7 +92,7 @@ describe('Loading scratch gui', () => {
|
||||
|
||||
test('Load sb3 project with a missing bmp costume', async () => {
|
||||
await loadUri(uri);
|
||||
await clickText('File');
|
||||
await clickXpath(FILE_MENU_XPATH);
|
||||
await clickText('Load from your computer');
|
||||
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
|
||||
await input.sendKeys(path.resolve(__dirname, '../fixtures/missing-bmp.sb3'));
|
||||
@@ -100,7 +103,7 @@ describe('Loading scratch gui', () => {
|
||||
|
||||
test('Load sb3 project with an invalid bmp costume', async () => {
|
||||
await loadUri(uri);
|
||||
await clickText('File');
|
||||
await clickXpath(FILE_MENU_XPATH);
|
||||
await clickText('Load from your computer');
|
||||
const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
|
||||
await input.sendKeys(path.resolve(__dirname, '../fixtures/corrupt-bmp.sb3'));
|
||||
|
||||
Reference in new issue
Block a user