diff --git a/package.json b/package.json index 778cc2eea..50369a1f7 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "react-style-proptype": "3.1.0", "react-tabs": "2.1.1", "react-test-renderer": "16.2.0", + "react-tooltip": "3.4.0", "redux": "3.7.0", "redux-mock-store": "^1.2.3", "redux-throttle": "0.1.1", diff --git a/src/components/button/button.jsx b/src/components/button/button.jsx index ae322d10f..59b39668f 100644 --- a/src/components/button/button.jsx +++ b/src/components/button/button.jsx @@ -9,23 +9,31 @@ const ButtonComponent = ({ onClick, children, ...props -}) => ( - - {children} - -); +}) => { + if (props.disabled === true) { + onClick = function () {}; + } + + return ( + + {children} + + ); +}; ButtonComponent.propTypes = { children: PropTypes.node, className: PropTypes.string, + disabled: PropTypes.bool, onClick: PropTypes.func.isRequired }; + export default ButtonComponent; diff --git a/src/components/coming-soon/aww-cat.png b/src/components/coming-soon/aww-cat.png new file mode 100644 index 000000000..bddfb8de9 Binary files /dev/null and b/src/components/coming-soon/aww-cat.png differ diff --git a/src/components/coming-soon/coming-soon.css b/src/components/coming-soon/coming-soon.css new file mode 100644 index 000000000..7c3bdeb14 --- /dev/null +++ b/src/components/coming-soon/coming-soon.css @@ -0,0 +1,68 @@ +/* + * NOTE: the copious use of `important` is needed to overwrite + * the default tooltip styling, and is required by the 3rd party + * library being used, `react-tooltip` + */ + +@import "../../css/colors.css"; + +.coming-soon { + background-color: $data-primary !important; + border: 1px solid hsla(0, 0%, 0%, .1) !important; + border-radius: .25rem !important; + box-shadow: 0 0 .5rem hsla(0, 0%, 0%, .25) !important; + padding: .75rem 1rem !important; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important; + font-size: 1rem !important; + line-height: 1.25rem !important; + z-index: 100 !important; +} + +.coming-soon:after { + content: ""; + border-top: 1px solid hsla(0, 0%, 0%, .1) !important; + border-left: 0 !important; + border-bottom: 0 !important; + border-right: 1px solid hsla(0, 0%, 0%, .1) !important; + border-radius: .25rem; + background-color: $data-primary !important; + height: 1rem !important; + width: 1rem !important; +} + +.show, +.show:before, +.show:after { + opacity: 1 !important; +} + +.left:after { + margin-top: -.5rem !important; + right: -.5rem !important; + transform: rotate(45deg) !important; +} + +.right:after { + margin-top: -.5rem !important; + left: -.5rem !important; + transform: rotate(-135deg) !important; +} + +.top:after { + margin-right: -.5rem !important; + bottom: -.5rem !important; + transform: rotate(135deg) !important; +} + +.bottom:after { + margin-left: -.5rem !important; + top: -.5rem !important; + transform: rotate(-45deg) !important; +} + +.coming-soon-image { + margin-left: .125rem; + width: 1.25rem; + height: 1.25rem; + vertical-align: middle; +} diff --git a/src/components/coming-soon/coming-soon.jsx b/src/components/coming-soon/coming-soon.jsx new file mode 100644 index 000000000..e16ae80be --- /dev/null +++ b/src/components/coming-soon/coming-soon.jsx @@ -0,0 +1,143 @@ +import bindAll from 'lodash.bindall'; +import classNames from 'classnames'; +import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl'; +import PropTypes from 'prop-types'; +import React from 'react'; +import ReactTooltip from 'react-tooltip'; + +import styles from './coming-soon.css'; + +import awwCatIcon from './aww-cat.png'; +import coolCatIcon from './cool-cat.png'; + +const messages = defineMessages({ + message1: { + defaultMessage: 'Don\'t worry, we\'re on it {emoji}', + description: 'One of the "coming soon" random messages for yet-to-be-done features', + id: 'gui.comingSoon.message1' + }, + message2: { + defaultMessage: 'Coming Soon...', + description: 'One of the "coming soon" random messages for yet-to-be-done features', + id: 'gui.comingSoon.message2' + }, + message3: { + defaultMessage: 'We\'re working on it {emoji}', + description: 'One of the "coming soon" random messages for yet-to-be-done features', + id: 'gui.comingSoon.message3' + } +}); + +class ComingSoonContent extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'setHide', + 'setShow', + 'getRandomMessage' + ]); + this.state = { + isShowing: false + }; + } + setShow () { + // needed to set the opacity to 1, since the default is .9 on show + this.setState({isShowing: true}); + } + setHide () { + this.setState({isShowing: false}); + } + getRandomMessage () { + // randomly chooses a messages from `messages` to display in the tooltip. + const images = [awwCatIcon, coolCatIcon]; + const messageNumber = Math.floor(Math.random() * Object.keys(messages).length) + 1; + const imageNumber = Math.floor(Math.random() * Object.keys(images).length); + return ( + + ) + }} + /> + ); + } + render () { + return ( + + ); + } +} + +ComingSoonContent.propTypes = { + className: PropTypes.string, + intl: intlShape, + place: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), + tooltipId: PropTypes.string.isRequired +}; + +ComingSoonContent.defaultProps = { + place: 'bottom' +}; + +const ComingSoon = injectIntl(ComingSoonContent); + +const ComingSoonTooltip = props => ( +
+
+ {props.children} +
+ +
+); + +ComingSoonTooltip.propTypes = { + children: PropTypes.node.isRequired, + className: PropTypes.string, + delayHide: PropTypes.number, + delayShow: PropTypes.number, + place: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), + tooltipClassName: PropTypes.string, + tooltipId: PropTypes.string.isRequired +}; + +ComingSoonTooltip.defaultProps = { + delayHide: 0, + delayShow: 0 +}; + +export { + ComingSoon as ComingSoonComponent, + ComingSoonTooltip +}; diff --git a/src/components/coming-soon/cool-cat.png b/src/components/coming-soon/cool-cat.png new file mode 100644 index 000000000..15b2151e3 Binary files /dev/null and b/src/components/coming-soon/cool-cat.png differ diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index 3d08cadf6..2c0533e98 100644 --- a/src/components/gui/gui.css +++ b/src/components/gui/gui.css @@ -143,14 +143,6 @@ overflow: hidden; } -.stage-menu-wrapper { - display: flex; - flex-shrink: 0; - align-items: center; - height: $stage-menu-height; - padding: $space; -} - .extension-button-container { width: 3.25rem; height: 3.25rem; diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 349e20d5e..639c8cbd3 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -112,13 +112,7 @@ const GUIComponent = props => { - {isFullSize => ( - - )} + {isFullSize => ( diff --git a/src/components/language-selector/language-icon.svg b/src/components/language-selector/language-icon.svg index 3644ab6fd..42bd409ab 100644 --- a/src/components/language-selector/language-icon.svg +++ b/src/components/language-selector/language-icon.svg @@ -1,72 +1,18 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + Language + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/language-selector/language-selector.css b/src/components/language-selector/language-selector.css index a060131a8..0ae9e0a10 100644 --- a/src/components/language-selector/language-selector.css +++ b/src/components/language-selector/language-selector.css @@ -1,3 +1,4 @@ +@import "../../css/colors.css"; @import "../../css/units.css"; .group { @@ -9,7 +10,7 @@ .language-icon { height: 2rem; - display:none; + display: none; } .language-select { @@ -19,7 +20,7 @@ user-select: none; outline: none; background: rgba(255, 255, 255, 0.5); - color: #3373cc; + color: $motion-tertiary; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } diff --git a/src/components/language-selector/language-selector.jsx b/src/components/language-selector/language-selector.jsx index d3d3bb48d..db846fef5 100644 --- a/src/components/language-selector/language-selector.jsx +++ b/src/components/language-selector/language-selector.jsx @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import Box from '../box/box.jsx'; +import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx'; import locales from 'scratch-l10n'; import languageIcon from './language-icon.svg'; import styles from './language-selector.css'; @@ -12,27 +13,33 @@ const LanguageSelector = ({ ...props }) => ( -
- - -
+ +
+ + +
+
); diff --git a/src/components/load-button/load-button.jsx b/src/components/load-button/load-button.jsx index dd94a2a11..0f580278c 100644 --- a/src/components/load-button/load-button.jsx +++ b/src/components/load-button/load-button.jsx @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import ButtonComponent from '../button/button.jsx'; +import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx'; import styles from './load-button.css'; @@ -13,13 +14,24 @@ const LoadButtonComponent = ({ ...props }) => ( - {title} - + + + {title} + + + ); diff --git a/src/components/menu-bar/menu-bar.css b/src/components/menu-bar/menu-bar.css index a9d3b37fa..a85926d39 100644 --- a/src/components/menu-bar/menu-bar.css +++ b/src/components/menu-bar/menu-bar.css @@ -1,3 +1,4 @@ +@import "../../css/colors.css"; @import "../../css/units.css"; .menu-bar { @@ -23,7 +24,7 @@ width: 100%; */ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - background-color: #4c97ff; + background-color: $motion-primary; color: white; } @@ -56,11 +57,6 @@ line-height: $menu-bar-height; cursor: pointer; text-decoration: none; - color: white; + color: $motion-tertiary; user-select: none; } - -.menu-item:hover { - opacity: 0.8; - background: rgba(255, 255, 255, 0.2) -} diff --git a/src/components/prompt/icon--dropdown-caret.svg b/src/components/prompt/icon--dropdown-caret.svg new file mode 100644 index 000000000..8e08eda35 --- /dev/null +++ b/src/components/prompt/icon--dropdown-caret.svg @@ -0,0 +1,12 @@ + + + + dropdown-caret-gray + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/src/components/prompt/prompt.css b/src/components/prompt/prompt.css index 94b907c04..527901233 100644 --- a/src/components/prompt/prompt.css +++ b/src/components/prompt/prompt.css @@ -49,3 +49,29 @@ .button-row button + button { margin-left: 0.5rem; } + +.more-options { + border-top: 1px dashed hsla(0, 0%, 0%, .25); + overflow: visible; + padding: 1rem; + text-align: center; + margin: 0 0 1rem; +} + +.more-options-accordion { + width: 60%; + margin: 0 auto; +} + +.more-options-text { + opacity: .5; +} + +.more-options-icon { + width: .75rem; + height: .75rem; + margin-left: .5rem; + vertical-align: middle; + padding-bottom: .2rem; + opacity: .5; +} diff --git a/src/components/prompt/prompt.jsx b/src/components/prompt/prompt.jsx index 4fca4cb4a..b99580b79 100644 --- a/src/components/prompt/prompt.jsx +++ b/src/components/prompt/prompt.jsx @@ -1,10 +1,23 @@ +import {defineMessages, FormattedMessage} from 'react-intl'; import PropTypes from 'prop-types'; import React from 'react'; -import Modal from '../modal/modal.jsx'; + import Box from '../box/box.jsx'; +import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx'; +import Modal from '../modal/modal.jsx'; import styles from './prompt.css'; +import dropdownIcon from './icon--dropdown-caret.svg'; + +const messages = defineMessages({ + moreOptionsMessage: { + defaultMessage: 'More Options', + description: 'Dropdown message for variable/list options', + id: 'gui.gui.variablePrompt' + } +}); + const PromptComponent = props => ( ( onKeyPress={props.onKeyPress} />
+ + +
+ + +
+
+
+
- - ) : ( - - - - + ); + } else { + header = ( + + + +
+
+ +
+ {props.intl.formatMessage(messages.smallStageSizeMessage)} +
+
+
+ +
+
+
+ +
+
+
-
- ); + ); + } + + return header; }; + StageHeaderComponent.propTypes = { - className: PropTypes.string, - height: PropTypes.number, - isZoomed: PropTypes.bool.isRequired, - onUnzoom: PropTypes.func.isRequired, - onZoom: PropTypes.func.isRequired, - titleZoomIcon: PropTypes.string, - vm: PropTypes.instanceOf(VM).isRequired, - width: PropTypes.number + intl: intlShape, + isFullScreen: PropTypes.bool.isRequired, + onSetStageFull: PropTypes.func.isRequired, + onSetStageLarge: PropTypes.func.isRequired, + onSetStageUnFull: PropTypes.func.isRequired, + stageSize: PropTypes.oneOf(Object.keys(STAGE_SIZES)).isRequired, + vm: PropTypes.instanceOf(VM).isRequired }; -StageHeaderComponent.defaultProps = { - width: 480, - height: 360, - titleZoomIcon: 'Zoom Control' -}; -export default StageHeaderComponent; + +export default injectIntl(StageHeaderComponent); diff --git a/src/components/stage/stage.jsx b/src/components/stage/stage.jsx index 47981e04a..c8777a263 100644 --- a/src/components/stage/stage.jsx +++ b/src/components/stage/stage.jsx @@ -13,7 +13,7 @@ const StageComponent = props => { canvasRef, height, isColorPicking, - isZoomed, + isFullScreen, width, colorInfo, onDeactivateColorPicker, @@ -26,7 +26,7 @@ const StageComponent = props => { let widthCorrectedAspect = width; const spacingBorderAdjustment = 9; const stageMenuHeightAdjustment = 40; - if (isZoomed) { + if (isFullScreen) { heightCorrectedAspect = window.innerHeight - stageMenuHeightAdjustment - spacingBorderAdjustment; widthCorrectedAspect = heightCorrectedAspect + (heightCorrectedAspect / 3); if (widthCorrectedAspect > window.innerWidth) { @@ -38,15 +38,15 @@ const StageComponent = props => {
- Save - + + Save + + ); } } diff --git a/src/containers/stage-header.jsx b/src/containers/stage-header.jsx index d3acba94e..593851a41 100644 --- a/src/containers/stage-header.jsx +++ b/src/containers/stage-header.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import VM from 'scratch-vm'; -import {setZoomed} from '../reducers/zoom'; +import {setStageSize, setFullScreen, STAGE_SIZES} from '../reducers/stage-size'; import {connect} from 'react-redux'; @@ -22,19 +22,20 @@ class StageHeader extends React.Component { } StageHeader.propTypes = { - height: PropTypes.number, - isZoomed: PropTypes.bool, - vm: PropTypes.instanceOf(VM).isRequired, - width: PropTypes.number + stageSize: PropTypes.oneOf(Object.keys(STAGE_SIZES)), + vm: PropTypes.instanceOf(VM).isRequired }; const mapStateToProps = state => ({ - isZoomed: state.isZoomed + stageSize: state.stageSize.stageSize, + isFullScreen: state.stageSize.isFullScreen }); const mapDispatchToProps = dispatch => ({ - onZoom: () => dispatch(setZoomed(true)), - onUnzoom: () => dispatch(setZoomed(false)) + onSetStageLarge: () => dispatch(setStageSize(STAGE_SIZES.large)), + onSetStageSmall: () => dispatch(setStageSize(STAGE_SIZES.small)), + onSetStageFull: () => dispatch(setFullScreen(true)), + onSetStageUnFull: () => dispatch(setFullScreen(false)) }); export default connect( diff --git a/src/containers/stage.jsx b/src/containers/stage.jsx index 9f76aa7ab..cb2a38963 100644 --- a/src/containers/stage.jsx +++ b/src/containers/stage.jsx @@ -57,7 +57,7 @@ class Stage extends React.Component { this.props.height !== nextProps.height || this.props.isColorPicking !== nextProps.isColorPicking || this.state.colorInfo !== nextState.colorInfo || - this.props.isZoomed !== nextProps.isZoomed || + this.props.isFullScreen !== nextProps.isFullScreen || this.state.question !== nextState.question; } componentDidUpdate (prevProps) { @@ -279,7 +279,7 @@ class Stage extends React.Component { Stage.propTypes = { height: PropTypes.number, isColorPicking: PropTypes.bool, - isZoomed: PropTypes.bool, + isFullScreen: PropTypes.bool.isRequired, onActivateColorPicker: PropTypes.func, onDeactivateColorPicker: PropTypes.func, vm: PropTypes.instanceOf(VM).isRequired, @@ -288,7 +288,7 @@ Stage.propTypes = { const mapStateToProps = state => ({ isColorPicking: state.colorPicker.active, - isZoomed: state.isZoomed + isFullScreen: state.stageSize.isFullScreen }); const mapDispatchToProps = dispatch => ({ diff --git a/src/css/colors.css b/src/css/colors.css index d524795cd..e270217f9 100644 --- a/src/css/colors.css +++ b/src/css/colors.css @@ -16,4 +16,6 @@ $sound-tertiary: #A63FA6; $control-primary: #FFAB19; +$data-primary: #FF8C1A; + $form-border: #E9EEF2; diff --git a/src/reducers/gui.js b/src/reducers/gui.js index 83ede0203..d49144b4f 100644 --- a/src/reducers/gui.js +++ b/src/reducers/gui.js @@ -8,14 +8,14 @@ import monitorLayoutReducer from './monitor-layout'; import targetReducer from './targets'; import toolboxReducer from './toolbox'; import vmReducer from './vm'; -import zoomReducer from './zoom'; +import stageSizeReducer from './stage-size'; import {ScratchPaintReducer} from 'scratch-paint'; export default combineReducers({ colorPicker: colorPickerReducer, customProcedures: customProceduresReducer, intl: intlReducer, - isZoomed: zoomReducer, + stageSize: stageSizeReducer, modals: modalReducer, monitors: monitorReducer, monitorLayout: monitorLayoutReducer, diff --git a/src/reducers/stage-size.js b/src/reducers/stage-size.js new file mode 100644 index 000000000..ea2750e34 --- /dev/null +++ b/src/reducers/stage-size.js @@ -0,0 +1,55 @@ +const SET_STAGE_SIZE = 'scratch-gui/StageSize/SET_STAGE_SIZE'; +const SET_FULL_SCREEN = 'scratch-gui/StageSize/SET_FULL_SCREEN'; + +const initialState = { + isFullScreen: false, + stageSize: 'large' +}; + +// stage size constants +const STAGE_SIZES = { + small: 'small', + large: 'large' +}; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case SET_STAGE_SIZE: + return { + isFullScreen: state.isFullScreen, + stageSize: action.stageSize + }; + case SET_FULL_SCREEN: + return { + isFullScreen: action.isFullScreen, + stageSize: state.stageSize + }; + default: + return state; + } +}; + +const setStageSize = function (stageSize) { + return { + type: SET_STAGE_SIZE, + stageSize: stageSize + }; +}; + +// `isFullScreen` is a separate value because "stage size" does not +// actually apply to full screen mode, so they are treated as separate +// values to be assessed. +const setFullScreen = function (isFullScreen) { + return { + type: SET_FULL_SCREEN, + isFullScreen: isFullScreen + }; +}; + +export { + reducer as default, + setStageSize, + setFullScreen, + STAGE_SIZES +}; diff --git a/src/reducers/zoom.js b/src/reducers/zoom.js deleted file mode 100644 index 3cd0f5f7f..000000000 --- a/src/reducers/zoom.js +++ /dev/null @@ -1,23 +0,0 @@ -const SET_ZOOMED = 'scratch-gui/Zoomed/SET_ZOOMED'; -const defaultZoomed = false; -const initialState = defaultZoomed; - -const reducer = function (state, action) { - if (typeof state === 'undefined') state = initialState; - switch (action.type) { - case SET_ZOOMED: - return action.isZoomed; - default: - return state; - } -}; -const setZoomed = function (isZoomed) { - return { - type: SET_ZOOMED, - isZoomed: isZoomed - }; -}; -export { - reducer as default, - setZoomed -}; diff --git a/test/integration/test.js b/test/integration/test.js index 58b488ecd..8668fce2a 100644 --- a/test/integration/test.js +++ b/test/integration/test.js @@ -143,7 +143,7 @@ describe('costumes, sounds and variables', () => { const projectId = '96708228'; await loadUri(`${uri}#${projectId}`); await new Promise(resolve => setTimeout(resolve, 2000)); - await clickXpath('//img[@title="Zoom Control"]'); + await clickXpath('//img[@title="Full Screen Control"]'); await clickXpath('//img[@title="Go"]'); await new Promise(resolve => setTimeout(resolve, 2000)); await clickXpath('//img[@title="Stop"]'); @@ -235,7 +235,9 @@ describe('costumes, sounds and variables', () => { await expect(logs).toEqual([]); }); - test('Localization', async () => { + // Skipped temporarily while the language selector is marked as + // "Coming Soon" + test.skip('Localization', async () => { await loadUri(uri); await clickText('Blocks'); await clickText('Extensions');