diff --git a/src/components/controls/controls.css b/src/components/controls/controls.css new file mode 100644 index 0000000000000000000000000000000000000000..7e60ae33419c73f93a82da5e6d1c21e7c87d6a6a --- /dev/null +++ b/src/components/controls/controls.css @@ -0,0 +1,3 @@ +.controls-container { + display: flex; +} diff --git a/src/components/controls/controls.jsx b/src/components/controls/controls.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8fec705b08ac9ad19a3d5a1669fded868b4b8a3a --- /dev/null +++ b/src/components/controls/controls.jsx @@ -0,0 +1,53 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import GreenFlag from '../green-flag/green-flag.jsx'; +import StopAll from '../stop-all/stop-all.jsx'; +import TurboMode from '../turbo-mode/turbo-mode.jsx'; + +import styles from './controls.css'; + +const Controls = function (props) { + const { + active, + className, + onGreenFlagClick, + onStopAllClick, + turbo, + ...componentProps + } = props; + return ( + <div + className={classNames(styles.controlsContainer, className)} + {...componentProps} + > + <GreenFlag + active={active} + onClick={onGreenFlagClick} + /> + <StopAll + active={active} + onClick={onStopAllClick} + /> + {turbo ? ( + <TurboMode /> + ) : null} + </div> + ); +}; + +Controls.propTypes = { + active: PropTypes.bool, + className: PropTypes.string, + onGreenFlagClick: PropTypes.func.isRequired, + onStopAllClick: PropTypes.func.isRequired, + turbo: PropTypes.bool +}; + +Controls.defaultProps = { + active: false, + turbo: false +}; + +export default Controls; diff --git a/src/components/green-flag/green-flag.css b/src/components/green-flag/green-flag.css index 3fda3a2072ac38f93a346d3515d6c22744d0d4d6..2c145283869ac0788d3e5be4f96e8b6afd04594f 100644 --- a/src/components/green-flag/green-flag.css +++ b/src/components/green-flag/green-flag.css @@ -1,13 +1,21 @@ .green-flag { - width: 1.1rem; - height: 1.1rem; - opacity: 0.5; + box-sizing: content-box; + width: 1.25rem; + height: 1.25rem; + padding: 0.375rem; + border-radius: 0.25rem; user-select: none; cursor: pointer; - transition: opacity 0.2s ease-out; /* @todo: standardize with var */ + transition: 0.2s ease-out; } -.green-flag.is-active, .green-flag:hover { - opacity: 1; + /* Scale flag image by 1.2, but keep background static */ + width: 1.5rem; + height: 1.5rem; + padding: 0.25rem; +} + +.green-flag.is-active { + background-color: rgba(0, 0, 0, 0.1); } diff --git a/src/components/green-flag/icon-green-flag.svg b/src/components/green-flag/icon-green-flag.svg deleted file mode 100644 index 4bd2528f9be697e4b8a35158c217fffe81c8309b..0000000000000000000000000000000000000000 Binary files a/src/components/green-flag/icon-green-flag.svg and /dev/null differ diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index 6a063a166edd3eedd74aeff3efc67874fda5f5e7..c8a9285a2a204bee92158053e3c268c4bc4967cd 100644 --- a/src/components/gui/gui.css +++ b/src/components/gui/gui.css @@ -101,10 +101,6 @@ position: relative; } -.green-flag { - margin: 0.25rem 0.6rem; -} - .stage-and-target-wrapper { /* Makes rows for children: @@ -130,7 +126,7 @@ display: flex; flex-grow: 1; flex-basis: 0; - + padding-top: $space; padding-left: $space; padding-right: $space; diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 8c8c9f96d8d00a3be9c9dc7a5b9136fb182a2405..1eb0e65054565751b964170a6cc989b46869364a 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -8,11 +8,10 @@ import VM from 'scratch-vm'; import Blocks from '../../containers/blocks.jsx'; import CostumeTab from '../../containers/costume-tab.jsx'; -import GreenFlag from '../../containers/green-flag.jsx'; +import Controls from '../../containers/controls.jsx'; import TargetPane from '../../containers/target-pane.jsx'; import SoundTab from '../../containers/sound-tab.jsx'; import Stage from '../../containers/stage.jsx'; -import StopAll from '../../containers/stop-all.jsx'; import Box from '../box/box.jsx'; import MenuBar from '../menu-bar/menu-bar.jsx'; @@ -90,11 +89,7 @@ const GUIComponent = props => { <Box className={styles.stageAndTargetWrapper}> <Box className={styles.stageMenuWrapper}> - <GreenFlag - className={styles.greenFlag} - vm={vm} - /> - <StopAll vm={vm} /> + <Controls vm={vm} /> </Box> <Box className={styles.stageWrapper}> <MediaQuery minWidth={layout.fullSizeMinWidth}>{isFullSize => ( diff --git a/src/components/stop-all/stop-all.css b/src/components/stop-all/stop-all.css index 95a63377110218a2977663572866915e599a537a..99ebee89284ebd481597ef86874083352b2dc352 100644 --- a/src/components/stop-all/stop-all.css +++ b/src/components/stop-all/stop-all.css @@ -1,13 +1,22 @@ .stop-all { - width: 1.1rem; - height: 1.1rem; - opacity: 0.5; + box-sizing: content-box; + width: 1.25rem; + height: 1.25rem; + padding: 0.375rem; + border-radius: 0.25rem; user-select: none; cursor: pointer; - transition: opacity 0.2s ease-out; /* @todo: standardize with var */ + transition: 0.2s ease-out; +} + +.stop-all { + opacity: 0.5; } -.stop-all.is-active, .stop-all:hover { + transform: scale(1.2); +} + +.stop-all.is-active { opacity: 1; } diff --git a/src/components/turbo-mode/icon--turbo.svg b/src/components/turbo-mode/icon--turbo.svg new file mode 100644 index 0000000000000000000000000000000000000000..64fd1665fc03cb694262fc1a67b1adc958e29f4b Binary files /dev/null and b/src/components/turbo-mode/icon--turbo.svg differ diff --git a/src/components/turbo-mode/turbo-mode.css b/src/components/turbo-mode/turbo-mode.css new file mode 100644 index 0000000000000000000000000000000000000000..d12699cee0764a66a5e60e86c7ec259239704571 --- /dev/null +++ b/src/components/turbo-mode/turbo-mode.css @@ -0,0 +1,20 @@ +@import "../../css/colors.css"; + +.turbo-container { + display: flex; + align-items: center; + padding: 0.25rem; + user-select: none; +} + +.turbo-icon { + margin: 0.25rem; +} + +.turbo-label { + font-size: 0.625rem; + font-weight: bold; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + color: $control-primary; + white-space: nowrap; +} diff --git a/src/components/turbo-mode/turbo-mode.jsx b/src/components/turbo-mode/turbo-mode.jsx new file mode 100644 index 0000000000000000000000000000000000000000..724af24d1e1527c2acb79de0df0083d3ef1de4e7 --- /dev/null +++ b/src/components/turbo-mode/turbo-mode.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import {FormattedMessage} from 'react-intl'; + +import turboIcon from './icon--turbo.svg'; + +import styles from './turbo-mode.css'; + +const TurboMode = () => ( + <div className={styles.turboContainer}> + <img + className={styles.turboIcon} + src={turboIcon} + /> + <div className={styles.turboLabel}> + <FormattedMessage + defaultMessage="Turbo Mode" + description="Label indicating turbo mode is active" + id="controls.turboMode" + /> + </div> + </div> +); + +export default TurboMode; diff --git a/src/containers/controls.jsx b/src/containers/controls.jsx new file mode 100644 index 0000000000000000000000000000000000000000..18dd44e5ea6c41204f706f665c88cfbb1c093b11 --- /dev/null +++ b/src/containers/controls.jsx @@ -0,0 +1,70 @@ +import bindAll from 'lodash.bindall'; +import PropTypes from 'prop-types'; +import React from 'react'; +import VM from 'scratch-vm'; + +import ControlsComponent from '../components/controls/controls.jsx'; + +class Controls extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleGreenFlagClick', + 'handleStopAllClick', + 'onProjectRunStart', + 'onProjectRunStop' + ]); + this.state = { + projectRunning: false, + turbo: false + }; + } + componentDidMount () { + this.props.vm.addListener('PROJECT_RUN_START', this.onProjectRunStart); + this.props.vm.addListener('PROJECT_RUN_STOP', this.onProjectRunStop); + } + componentWillUnmount () { + this.props.vm.removeListener('PROJECT_RUN_START', this.onProjectRunStart); + this.props.vm.removeListener('PROJECT_RUN_STOP', this.onProjectRunStop); + } + onProjectRunStart () { + this.setState({projectRunning: true}); + } + onProjectRunStop () { + this.setState({projectRunning: false}); + } + handleGreenFlagClick (e) { + e.preventDefault(); + if (e.shiftKey) { + this.setState({turbo: !this.state.turbo}); + this.props.vm.setTurboMode(!this.state.turbo); + } else { + this.props.vm.greenFlag(); + } + } + handleStopAllClick (e) { + e.preventDefault(); + this.props.vm.stopAll(); + } + render () { + const { + vm, // eslint-disable-line no-unused-vars + ...props + } = this.props; + return ( + <ControlsComponent + {...props} + active={this.state.projectRunning} + turbo={this.state.turbo} + onGreenFlagClick={this.handleGreenFlagClick} + onStopAllClick={this.handleStopAllClick} + /> + ); + } +} + +Controls.propTypes = { + vm: PropTypes.instanceOf(VM) +}; + +export default Controls; diff --git a/src/containers/green-flag.jsx b/src/containers/green-flag.jsx index b1f6acabf620c7b54f431f1f1e0e65821a580be5..a725b7e1ba6d198f69cc36da32c676ac9e350d12 100644 --- a/src/containers/green-flag.jsx +++ b/src/containers/green-flag.jsx @@ -11,24 +11,18 @@ class GreenFlag extends React.Component { super(props); bindAll(this, [ 'handleClick', - 'handleKeyDown', - 'handleKeyUp', 'onProjectRunStart', 'onProjectRunStop' ]); - this.state = {projectRunning: false, shiftKeyDown: false}; + this.state = {projectRunning: false}; } componentDidMount () { this.props.vm.addListener('PROJECT_RUN_START', this.onProjectRunStart); this.props.vm.addListener('PROJECT_RUN_STOP', this.onProjectRunStop); - document.addEventListener('keydown', this.handleKeyDown); - document.addEventListener('keyup', this.handleKeyUp); } componentWillUnmount () { this.props.vm.removeListener('PROJECT_RUN_START', this.onProjectRunStart); this.props.vm.removeListener('PROJECT_RUN_STOP', this.onProjectRunStop); - document.removeEventListener('keydown', this.handleKeyDown); - document.removeEventListener('keyup', this.handleKeyUp); } onProjectRunStart () { this.setState({projectRunning: true}); @@ -36,15 +30,9 @@ class GreenFlag extends React.Component { onProjectRunStop () { this.setState({projectRunning: false}); } - handleKeyDown (e) { - this.setState({shiftKeyDown: e.shiftKey}); - } - handleKeyUp (e) { - this.setState({shiftKeyDown: e.shiftKey}); - } handleClick (e) { e.preventDefault(); - if (this.state.shiftKeyDown) { + if (e.shiftKey) { this.props.vm.setTurboMode(!this.props.vm.runtime.turboMode); } else { this.props.vm.greenFlag(); diff --git a/src/css/colors.css b/src/css/colors.css index ea3f5936f3219493eddf95bccd01ba50d29e0ffc..d524795cd5ece9550b0aee9767111e60d43c2f91 100644 --- a/src/css/colors.css +++ b/src/css/colors.css @@ -14,4 +14,6 @@ $red-tertiary: #E64D00; $sound-primary: #CF63CF; $sound-tertiary: #A63FA6; +$control-primary: #FFAB19; + $form-border: #E9EEF2; diff --git a/src/examples/blocks-only.css b/src/examples/blocks-only.css index 9713613886c9caa437135a932fb4375be6db5b96..0f3ac07f8504fd62cbf72ad1245a0b9ca0946581 100644 --- a/src/examples/blocks-only.css +++ b/src/examples/blocks-only.css @@ -1,11 +1,4 @@ -.green-flag { - position: absolute; - z-index: 2; - top: 10px; - right: 50px; -} - -.stop-all { +.controls { position: absolute; z-index: 2; top: 10px; diff --git a/src/examples/blocks-only.jsx b/src/examples/blocks-only.jsx index c97444d9ec1ffdf318d66089da35b79470399dc9..8d5159e825105374827e818ce7009e57587e87de 100644 --- a/src/examples/blocks-only.jsx +++ b/src/examples/blocks-only.jsx @@ -3,8 +3,7 @@ import ReactDOM from 'react-dom'; import {connect} from 'react-redux'; import AppStateHOC from '../lib/app-state-hoc.jsx'; -import GreenFlag from '../containers/green-flag.jsx'; -import StopAll from '../containers/stop-all.jsx'; +import Controls from '../containers/controls.jsx'; import Blocks from '../containers/blocks.jsx'; import GUI from '../containers/gui.jsx'; import ProjectLoaderHOC from '../lib/project-loader-hoc.jsx'; @@ -14,8 +13,7 @@ import styles from './blocks-only.css'; const mapStateToProps = state => ({vm: state.vm}); const VMBlocks = connect(mapStateToProps)(Blocks); -const VMGreenFlag = connect(mapStateToProps)(GreenFlag); -const VMStopAll = connect(mapStateToProps)(StopAll); +const VMControls = connect(mapStateToProps)(Controls); const BlocksOnly = props => ( <GUI {...props}> @@ -25,8 +23,7 @@ const BlocksOnly = props => ( media: `static/blocks-media/` }} /> - <VMGreenFlag className={styles.greenFlag} /> - <VMStopAll className={styles.stopAll} /> + <VMControls className={styles.controls} /> </GUI> ); diff --git a/src/examples/player.jsx b/src/examples/player.jsx index 56c7a73607256c69bfebc57208d166ee60718c0f..ab50527cde1ae1d4be7788e7545397ff48b3700c 100644 --- a/src/examples/player.jsx +++ b/src/examples/player.jsx @@ -3,8 +3,7 @@ import ReactDOM from 'react-dom'; import {connect} from 'react-redux'; import AppStateHOC from '../lib/app-state-hoc.jsx'; -import GreenFlag from '../containers/green-flag.jsx'; -import StopAll from '../containers/stop-all.jsx'; +import Controls from '../containers/controls.jsx'; import Stage from '../containers/stage.jsx'; import Box from '../components/box/box.jsx'; import GUI from '../containers/gui.jsx'; @@ -15,8 +14,7 @@ import './player.css'; const mapStateToProps = state => ({vm: state.vm}); const VMStage = connect(mapStateToProps)(Stage); -const VMGreenFlag = connect(mapStateToProps)(GreenFlag); -const VMStopAll = connect(mapStateToProps)(StopAll); +const VMControls = connect(mapStateToProps)(Controls); class Player extends React.Component { constructor (props) { @@ -55,17 +53,12 @@ class Player extends React.Component { width={width} > <Box height={40}> - <VMGreenFlag + <VMControls style={{ marginRight: 10, height: 40 }} /> - <VMStopAll - style={{ - height: 40 - }} - /> </Box> <VMStage height={height} diff --git a/test/unit/components/controls.test.jsx b/test/unit/components/controls.test.jsx new file mode 100644 index 0000000000000000000000000000000000000000..fcf22208d410c9815af9705b0941b40883fa8673 --- /dev/null +++ b/test/unit/components/controls.test.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import {shallowWithIntl} from '../../helpers/intl-helpers.jsx'; +import Controls from '../../../src/components/controls/controls'; +import TurboMode from '../../../src/components/turbo-mode/turbo-mode'; + +describe('Controls component', () => { + const defaultProps = () => ({ + active: false, + greenFlagTitle: 'Go', + onGreenFlagClick: jest.fn(), + onStopAllClick: jest.fn(), + stopAllTitle: 'Stop', + turbo: false + }); + + test('shows turbo mode when in turbo mode', () => { + const component = shallowWithIntl( + <Controls + {...defaultProps()} + /> + ); + expect(component.find(TurboMode).exists()).toEqual(false); + component.setProps({turbo: true}); + expect(component.find(TurboMode).exists()).toEqual(true); + }); + + test('triggers the right callbacks when clicked', () => { + const props = defaultProps(); + const component = shallowWithIntl( + <Controls + {...props} + /> + ); + component.find('[title="Go"]').simulate('click'); + expect(props.onGreenFlagClick).toHaveBeenCalled(); + + component.find('[title="Stop"]').simulate('click'); + expect(props.onStopAllClick).toHaveBeenCalled(); + }); +});