diff --git a/src/components/language-selector/language-selector.css b/src/components/language-selector/language-selector.css index 5da79cf25f3375ae6a00bdcbde105205c2e4dc61..dd09a7c7e76e45bf1c4e2ecfede707bc0381926f 100644 --- a/src/components/language-selector/language-selector.css +++ b/src/components/language-selector/language-selector.css @@ -2,10 +2,20 @@ @import "../../css/units.css"; .group { - display: inline-flex; - flex-direction: row; /* makes columns, for each label/form group */ - align-items: center; - vertical-align: middle; + z-index: 50; + top: $menu-bar-height; + left: 0; + position: absolute; + border: 1px solid $ui-black-transparent; + border-radius: 0 0 8px 8px; + background-color: $motion-primary; + padding: 0; + margin: 0; + min-width: 186px; + max-width: 260px; + overflow: visible; + color: $ui-white; + box-shadow: 0 8px 8px 0 $ui-black-transparent; } .language-icon { @@ -17,7 +27,8 @@ } .language-select { - width: 100%; + margin: .5rem; + width: calc(100% - 1rem); height: 1.85rem; border: 1px solid $motion-primary; user-select: none; diff --git a/src/components/language-selector/language-selector.jsx b/src/components/language-selector/language-selector.jsx index befe4797681a1097ad47c05f360129b045658b65..07249277fb1d27c841ba3926f5060d7e3ef5260b 100644 --- a/src/components/language-selector/language-selector.jsx +++ b/src/components/language-selector/language-selector.jsx @@ -1,56 +1,49 @@ -import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; import Box from '../box/box.jsx'; import locales from 'scratch-l10n'; -import languageIcon from './language-icon.svg'; -import dropdownCaret from './dropdown-caret.svg'; import styles from './language-selector.css'; -const LanguageSelector = ({ - currentLocale, - onChange, - open, - ...props -}) => ( - <Box {...props}> - <div className={styles.group}> - {open ? ( - <select - disabled - aria-label="language selector" - className={styles.languageSelect} - value={currentLocale} - onChange={onChange} +class LanguageSelector extends React.Component { + render () { + const { + componentRef, + currentLocale, + onChange, + ...componentProps + } = this.props; + return ( + <Box + {...componentProps} + > + <div + className={styles.group} + ref={componentRef} > - {Object.keys(locales).map(locale => ( - <option - key={locale} - value={locale} - > - {locales[locale].name} - </option> - ))} - </select> - ) : ( - <React.Fragment> - <img - className={classNames(styles.languageIcon, styles.disabled)} - src={languageIcon} - /> - <img - className={classNames(styles.dropdownCaret, styles.disabled)} - src={dropdownCaret} - /> - </React.Fragment> - )} - </div> - </Box> -); - + <select + aria-label="language selector" + className={styles.languageSelect} + value={currentLocale} + onChange={onChange} + > + {Object.keys(locales).map(locale => ( + <option + key={locale} + value={locale} + > + {locales[locale].name} + </option> + ))} + </select> + </div> + </Box> + ); + } +} LanguageSelector.propTypes = { + componentRef: PropTypes.func, currentLocale: PropTypes.string, onChange: PropTypes.func, open: PropTypes.bool diff --git a/src/components/menu-bar/menu-bar.css b/src/components/menu-bar/menu-bar.css index 8063361d9f5701d2788274f6b8b360ec31bf6db8..eba5f885eff82eae922578bdb57acaa75efbecd0 100644 --- a/src/components/menu-bar/menu-bar.css +++ b/src/components/menu-bar/menu-bar.css @@ -44,6 +44,14 @@ vertical-align: middle; } +.language-icon { + height: 1.5rem; +} + +.language-menu { + display: inline-flex; +} + .menu { z-index: $z-index-menu-bar; top: $menu-bar-height; diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx index 7742e210e362ac0d1772e98c447cb792ecec8a46..aa718b1b04cfc438a2615b7ad893a05cce5d5c85 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -22,7 +22,10 @@ import { fileMenuOpen, openEditMenu, closeEditMenu, - editMenuOpen + editMenuOpen, + openLanguageMenu, + closeLanguageMenu, + languageMenuOpen } from '../../reducers/menus'; import styles from './menu-bar.css'; @@ -32,6 +35,8 @@ import feedbackIcon from './icon--feedback.svg'; import profileIcon from './icon--profile.png'; import communityIcon from './icon--see-community.svg'; import dropdownCaret from '../language-selector/dropdown-caret.svg'; +import languageIcon from '../language-selector/language-icon.svg'; + import scratchLogo from './scratch-logo.svg'; import helpIcon from './icon--help.svg'; @@ -39,22 +44,34 @@ import helpIcon from './icon--help.svg'; const MenuBarItemTooltip = ({ children, className, + enable, id, place = 'bottom' -}) => ( - <ComingSoonTooltip - className={classNames(styles.comingSoon, className)} - place={place} - tooltipClassName={styles.comingSoonTooltip} - tooltipId={id} - > - {children} - </ComingSoonTooltip> -); +}) => { + if (enable) { + return ( + <React.Fragment> + {children} + </React.Fragment> + ); + } + return ( + <ComingSoonTooltip + className={classNames(styles.comingSoon, className)} + place={place} + tooltipClassName={styles.comingSoonTooltip} + tooltipId={id} + > + {children} + </ComingSoonTooltip> + ); +}; + MenuBarItemTooltip.propTypes = { children: PropTypes.node, className: PropTypes.string, + enable: PropTypes.bool, id: PropTypes.string, place: PropTypes.oneOf(['top', 'bottom', 'left', 'right']) }; @@ -111,12 +128,31 @@ const MenuBar = props => ( src={scratchLogo} /> </div> - <div className={classNames(styles.menuBarItem, styles.hoverable)}> + <div + className={classNames(styles.menuBarItem, styles.hoverable, { + [styles.active]: props.languageMenuOpen + })} + onMouseUp={props.onClickLanguage} + > <MenuBarItemTooltip + enable={window.location.search.indexOf('enable=language') !== -1} id="menubar-selector" place="right" > - <LanguageSelector /> + <div className={classNames(styles.languageMenu)}> + <img + className={styles.languageIcon} + src={languageIcon} + /> + <img + className={styles.dropdownCaret} + src={dropdownCaret} + /> + </div> + <LanguageSelector + open={props.languageMenuOpen} + onRequestClose={props.onRequestCloseLanguage} + /> </MenuBarItemTooltip> </div> <div @@ -369,17 +405,21 @@ MenuBar.propTypes = { editMenuOpen: PropTypes.bool, enableCommunity: PropTypes.bool, fileMenuOpen: PropTypes.bool, + languageMenuOpen: PropTypes.bool, onClickEdit: PropTypes.func, onClickFile: PropTypes.func, + onClickLanguage: PropTypes.func, onOpenTipLibrary: PropTypes.func, onRequestCloseEdit: PropTypes.func, onRequestCloseFile: PropTypes.func, + onRequestCloseLanguage: PropTypes.func, onSeeCommunity: PropTypes.func }; const mapStateToProps = state => ({ fileMenuOpen: fileMenuOpen(state), - editMenuOpen: editMenuOpen(state) + editMenuOpen: editMenuOpen(state), + languageMenuOpen: languageMenuOpen(state) }); const mapDispatchToProps = dispatch => ({ @@ -388,6 +428,8 @@ const mapDispatchToProps = dispatch => ({ onRequestCloseFile: () => dispatch(closeFileMenu()), onClickEdit: () => dispatch(openEditMenu()), onRequestCloseEdit: () => dispatch(closeEditMenu()), + onClickLanguage: () => dispatch(openLanguageMenu()), + onRequestCloseLanguage: () => dispatch(closeLanguageMenu()), onSeeCommunity: () => dispatch(setPlayer(true)) }); diff --git a/src/containers/language-selector.jsx b/src/containers/language-selector.jsx index 316b1577a367a4314ee7a610f72b9bee2516874d..1a1d7bf0d3d5818744318b99415224f62fb9e1c2 100644 --- a/src/containers/language-selector.jsx +++ b/src/containers/language-selector.jsx @@ -1,18 +1,85 @@ +import bindAll from 'lodash.bindall'; +import PropTypes from 'prop-types'; +import React from 'react'; import {connect} from 'react-redux'; +import {updateIntl} from 'react-intl-redux'; +import {closeLanguageMenu} from '../reducers/menus'; import LanguageSelectorComponent from '../components/language-selector/language-selector.jsx'; +class LanguageSelector extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'addListeners', + 'removeListeners', + 'handleChange', + 'handleClick', + 'ref' + ]); + if (props.open) this.addListeners(); + } + componentDidUpdate (prevProps) { + if (this.props.open && !prevProps.open) this.addListeners(); + if (!this.props.open && prevProps.open) this.removeListeners(); + } + addListeners () { + document.addEventListener('mouseup', this.handleClick); + } + removeListeners () { + document.removeEventListener('mouseup', this.handleClick); + } + handleChange (e) { + this.props.onChangeLanguage(e.target.value); + } + handleClick (e) { + if (this.props.open && this.selector && !this.selector.contains(e.target)) { + this.props.onRequestClose(); + } + } + ref (c) { + this.selector = c; + } + render () { + const { + open, + onChangeLanguage, // eslint-disable-line no-unused-vars + onRequestClose, // eslint-disable-line no-unused-vars + children, + ...props + } = this.props; + if (!open) return null; + return ( + <LanguageSelectorComponent + componentRef={this.ref} + onChange={this.handleChange} + {...props} + > + {children} + </LanguageSelectorComponent> + ); + } +} + +LanguageSelector.propTypes = { + children: PropTypes.node, + onChangeLanguage: PropTypes.func.isRequired, + onRequestClose: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired +}; + const mapStateToProps = state => ({ currentLocale: state.intl.locale }); -const mapDispatchToProps = () => ({ - onChange: e => { - e.preventDefault(); +const mapDispatchToProps = dispatch => ({ + onChangeLanguage: locale => { + dispatch(updateIntl({locale: locale, messages: {}})); + dispatch(closeLanguageMenu()); } }); export default connect( mapStateToProps, mapDispatchToProps -)(LanguageSelectorComponent); +)(LanguageSelector); diff --git a/src/reducers/menus.js b/src/reducers/menus.js index 9d9ab0be9882e9001c5bd8f63195d9755a82631d..6693160c177fc988232fbd505f5a6c1e5af8d134 100644 --- a/src/reducers/menus.js +++ b/src/reducers/menus.js @@ -3,11 +3,13 @@ const CLOSE_MENU = 'scratch-gui/menus/CLOSE_MENU'; const MENU_FILE = 'fileMenu'; const MENU_EDIT = 'editMenu'; +const MENU_LANGUAGE = 'languageMenu'; const initialState = { [MENU_FILE]: false, - [MENU_EDIT]: false + [MENU_EDIT]: false, + [MENU_LANGUAGE]: false }; const reducer = function (state, action) { @@ -39,6 +41,9 @@ 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 openLanguageMenu = () => openMenu(MENU_LANGUAGE); +const closeLanguageMenu = () => closeMenu(MENU_LANGUAGE); +const languageMenuOpen = state => state.scratchGui.menus[MENU_LANGUAGE]; export { reducer as default, @@ -47,6 +52,9 @@ export { closeFileMenu, openEditMenu, closeEditMenu, + openLanguageMenu, + closeLanguageMenu, fileMenuOpen, - editMenuOpen + editMenuOpen, + languageMenuOpen };