diff --git a/package.json b/package.json index e6458829d9250f47ab4ce9016b304672b737469d..0e03879a3a9b71cdfabf434aee6eff9a475872a9 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "lodash.pick": "4.4.0", "minilog": "3.1.0", "mkdirp": "^0.5.1", + "platform": "^1.3.4", "postcss-import": "^11.0.0", "postcss-loader": "^2.0.5", "postcss-simple-vars": "^4.0.0", diff --git a/src/components/browser-modal/browser-modal.css b/src/components/browser-modal/browser-modal.css new file mode 100644 index 0000000000000000000000000000000000000000..52773e08f1b90e4bc5a453b730ddfb04225b5af6 --- /dev/null +++ b/src/components/browser-modal/browser-modal.css @@ -0,0 +1,70 @@ +@import "../../css/colors.css"; +@import "../../css/units.css"; +@import "../../css/typography.css"; + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; + background-color: hsla(215, 100%, 65%, .9); +} + +.modal-content { + margin: 100px auto; + outline: none; + border: .25rem solid hsla(0, 100%, 100%, .25); + padding: 0; + border-radius: $space; + user-select: none; + width: 500px; + + color: $text-primary; + overflow: hidden; +} + +.illustration { + width: 100%; + height: 208px; + background-color: $motion-primary; + background-image: url('./unsupported.png'); + background-size: cover; +} + +.body { + background: $ui-pane-gray; + padding: 1.5rem 2.25rem; + text-align: center; +} + +/* Confirmation buttons at the bottom of the modal */ +.button-row { + margin: 1.5rem 0; + font-weight: bolder; + text-align: right; + display: flex; + justify-content: center; +} + +.button-row button { + border: 1px solid $motion-primary; + border-radius: 0.25rem; + padding: 0.5rem 2rem; + background: $motion-primary; + color: white; + font-weight: bold; + font-size: 0.875rem; +} + +.faq-link-text { + margin: 2rem 0 .5rem 0; + font-size: .875rem; + color: $text-primary; +} + +.faq-link { + color: $motion-primary; + text-decoration: none; +} diff --git a/src/components/browser-modal/browser-modal.jsx b/src/components/browser-modal/browser-modal.jsx new file mode 100644 index 0000000000000000000000000000000000000000..c410f7f62d26e359a300e2bb80072e8ed9bcf248 --- /dev/null +++ b/src/components/browser-modal/browser-modal.jsx @@ -0,0 +1,84 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import ReactModal from 'react-modal'; +import Box from '../box/box.jsx'; +import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl'; + +import styles from './browser-modal.css'; + +const messages = defineMessages({ + label: { + id: 'gui.unsupportedBrowser.label', + defaultMessage: 'Internet Explorer is not supported', + description: '' + } +}); + +const BrowserModal = ({intl, ...props}) => ( + <ReactModal + isOpen + className={styles.modalContent} + contentLabel={intl.formatMessage({...messages.label})} + overlayClassName={styles.modalOverlay} + onRequestClose={props.onBack} + > + <Box className={styles.illustration} /> + + <Box className={styles.body}> + <h2> + <FormattedMessage {...messages.label} /> + </h2> + <p> + { /* eslint-disable max-len */ } + <FormattedMessage + defaultMessage="We're very sorry, but Scratch 3.0 does not support Internet Explorer. We recommend trying a newer browser such as Google Chrome, Mozilla Firefox, or Microsoft Edge." + description="Unsupported browser description" + id="gui.unsupportedBrowser.description" + /> + { /* eslint-enable max-len */ } + </p> + + <Box className={styles.buttonRow}> + <button + className={styles.backButton} + onClick={props.onBack} + > + <FormattedMessage + defaultMessage="Back" + description="Label for button go back when browser is unsupported" + id="gui.unsupportedBrowser.back" + /> + </button> + + </Box> + <div className={styles.faqLinkText}> + <FormattedMessage + defaultMessage="To learn more, go to the {previewFaqLink}." + description="Scratch 3.0 FAQ description" + id="gui.unsupportedBrowser.previewfaq" + values={{ + previewFaqLink: ( + <a + className={styles.faqLink} + href="//scratch.mit.edu/preview-faq" + > + <FormattedMessage + defaultMessage="preview FAQ" + description="link to Scratch 3.0 FAQ page" + id="gui.unsupportedBrowser.previewfaqlink" + /> + </a> + ) + }} + /> + </div> + </Box> + </ReactModal> +); + +BrowserModal.propTypes = { + intl: intlShape.isRequired, + onBack: PropTypes.func.isRequired +}; + +export default injectIntl(BrowserModal); diff --git a/src/components/browser-modal/unsupported.png b/src/components/browser-modal/unsupported.png new file mode 100644 index 0000000000000000000000000000000000000000..d8fbab2f10ad965933527e6af8ecdf56cb3e3933 Binary files /dev/null and b/src/components/browser-modal/unsupported.png differ diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 639c8cbd30c12432512ba5886bc92f1931b02cba..e5164ff5734cb7fc4c12d76269f674c4f17222ad 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -13,6 +13,7 @@ import SoundTab from '../../containers/sound-tab.jsx'; import StageHeader from '../../containers/stage-header.jsx'; import Stage from '../../containers/stage.jsx'; import {FormattedMessage} from 'react-intl'; +import PreviewModal from '../../containers/preview-modal.jsx'; import Box from '../box/box.jsx'; import IconButton from '../icon-button/icon-button.jsx'; @@ -36,6 +37,7 @@ const GUIComponent = props => { children, enableExtensions, vm, + previewInfoVisible, onExtensionButtonClick, onTabSelect, tabIndex, @@ -63,6 +65,9 @@ const GUIComponent = props => { className={styles.pageWrapper} {...componentProps} > + {previewInfoVisible ? ( + <PreviewModal /> + ) : null} <MenuBar /> <Box className={styles.bodyWrapper}> <Box className={styles.flexWrapper}> @@ -141,6 +146,7 @@ GUIComponent.propTypes = { enableExtensions: PropTypes.bool, onExtensionButtonClick: PropTypes.func, onTabSelect: PropTypes.func, + previewInfoVisible: PropTypes.bool, tabIndex: PropTypes.number, vm: PropTypes.instanceOf(VM).isRequired }; diff --git a/src/components/preview-modal/happy-cat.svg b/src/components/preview-modal/happy-cat.svg new file mode 100644 index 0000000000000000000000000000000000000000..9a04aa18da28b7a787ad4217c81beeb806182578 Binary files /dev/null and b/src/components/preview-modal/happy-cat.svg differ diff --git a/src/components/preview-modal/info.png b/src/components/preview-modal/info.png new file mode 100644 index 0000000000000000000000000000000000000000..5b17f14bf158ad4766b047ec19a9ccb2c1d63bb6 Binary files /dev/null and b/src/components/preview-modal/info.png differ diff --git a/src/components/preview-modal/preview-modal.css b/src/components/preview-modal/preview-modal.css new file mode 100644 index 0000000000000000000000000000000000000000..b630741ec5c6fa58d627482a057b8780085979d5 --- /dev/null +++ b/src/components/preview-modal/preview-modal.css @@ -0,0 +1,88 @@ +@import "../../css/colors.css"; +@import "../../css/units.css"; +@import "../../css/typography.css"; + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; + background-color: hsla(215, 100%, 65%, .9); +} + +.modal-content { + margin: 100px auto; + outline: none; + border: .25rem solid hsla(0, 100%, 100%, .25); + padding: 0; + border-radius: $space; + user-select: none; + width: 500px; + + color: $text-primary; + overflow: hidden; +} + +.illustration { + width: 100%; + height: 208px; + background-color: $motion-primary; + background-image: url('./info.png'); + background-size: cover; +} + +.body { + background: $ui-pane-gray; + padding: 1.5rem 2.25rem; + text-align: center; +} + +/* Confirmation buttons at the bottom of the modal */ +.button-row { + margin: 1.5rem 0; + font-weight: bolder; + text-align: right; + display: flex; + justify-content: center; +} + +.button-row button { + border: 1px solid $motion-primary; + border-radius: 0.25rem; + padding: 0.5rem 2rem; + background: white; + font-weight: bold; + font-size: .875rem; +} + +.button-row button.ok-button { + background: $motion-primary; + color: white; +} + +.button-row button.no-button { + color: $motion-primary; +} +.button-row button + button { + margin-left: 0.5rem; +} + +.cat-icon { + margin-left: .125rem; + width: 1.5rem; + height: 1.5rem; + vertical-align: middle; +} + +.faq-link-text { + margin: 2rem 0 .5rem 0; + font-size: .875rem; + color: $text-primary; +} + +.faq-link { + color: $motion-primary; + text-decoration: none; +} diff --git a/src/components/preview-modal/preview-modal.jsx b/src/components/preview-modal/preview-modal.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0c1651a2a232c6f78f89df6bdbfc29319250b9fb --- /dev/null +++ b/src/components/preview-modal/preview-modal.jsx @@ -0,0 +1,106 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import ReactModal from 'react-modal'; +import Box from '../box/box.jsx'; +import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl'; + +import styles from './preview-modal.css'; +import catIcon from './happy-cat.svg'; + +const messages = defineMessages({ + label: { + id: 'gui.previewInfo.label', + defaultMessage: 'Try Scratch 3.0', + description: 'Scratch 3.0 modal label - for accessibility' + } +}); + +const PreviewModal = ({intl, ...props}) => ( + <ReactModal + isOpen + className={styles.modalContent} + contentLabel={intl.formatMessage({...messages.label})} + overlayClassName={styles.modalOverlay} + onRequestClose={props.onTryIt} + > + <Box className={styles.illustration} /> + + <Box className={styles.body}> + <h2> + <FormattedMessage + defaultMessage="Welcome to the Scratch 3.0 Preview" + description="Header for Preview Info Modal" + id="gui.previewInfo.welcome" + /> + </h2> + <p> + <FormattedMessage + defaultMessage="We're working on the next generation of Scratch. We're excited for you to try it!" + description="Invitation to try 3.0 preview" + id="gui.previewInfo.invitation" + /> + </p> + + <Box className={styles.buttonRow}> + <button + className={styles.noButton} + onClick={props.onCancel} + > + <FormattedMessage + defaultMessage="Not Now" + description="Label for button to back out of trying Scratch 3.0 preview" + id="gui.previewInfo.notnow" + /> + </button> + <button + className={styles.okButton} + title="tryit" + onClick={props.onTryIt} + > + <FormattedMessage + defaultMessage="Try It! {caticon}" + description="Label for button to try Scratch 3.0 preview" + id="gui.previewModal.tryit" + values={{ + caticon: ( + <img + className={styles.catIcon} + src={catIcon} + /> + ) + }} + /> + </button> + </Box> + <Box className={styles.faqLinkText}> + <FormattedMessage + defaultMessage="To learn more, go to the {previewFaqLink}." + description="Invitation to try 3.0 preview" + id="gui.previewInfo.previewfaq" + values={{ + previewFaqLink: ( + <a + className={styles.faqLink} + href="//scratch.mit.edu/preview-faq" + > + <FormattedMessage + defaultMessage="Preview FAQ" + description="link to Scratch 3.0 preview FAQ page" + id="gui.previewInfo.previewfaqlink" + /> + </a> + ) + }} + /> + </Box> + </Box> + </ReactModal> +); + +PreviewModal.propTypes = { + intl: intlShape.isRequired, + onCancel: PropTypes.func.isRequired, + onTryIt: PropTypes.func.isRequired +}; + +export default injectIntl(PreviewModal); diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index 9de0e3616691b9a931ebf3d10a4ac7bebf35feb6..1ed5c4165e4c4aa82ee5cd424cced84e63a8b923 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -60,13 +60,16 @@ class GUI extends React.Component { GUI.propTypes = { ...GUIComponent.propTypes, + previewInfoVisible: PropTypes.bool, projectData: PropTypes.string, vm: PropTypes.instanceOf(VM) }; GUI.defaultProps = GUIComponent.defaultProps; -const mapStateToProps = () => ({}); +const mapStateToProps = state => ({ + previewInfoVisible: state.modals.previewInfo +}); const mapDispatchToProps = dispatch => ({ onExtensionButtonClick: () => dispatch(openExtensionLibrary()) diff --git a/src/containers/preview-modal.jsx b/src/containers/preview-modal.jsx new file mode 100644 index 0000000000000000000000000000000000000000..db913e9ea709c170c1c94c609e9ce9c93e5f12ff --- /dev/null +++ b/src/containers/preview-modal.jsx @@ -0,0 +1,68 @@ +import bindAll from 'lodash.bindall'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {connect} from 'react-redux'; +import platform from 'platform'; + +import PreviewModalComponent from '../components/preview-modal/preview-modal.jsx'; +import BrowserModalComponent from '../components/browser-modal/browser-modal.jsx'; + +import { + closePreviewInfo +} from '../reducers/modals'; + +class PreviewModal extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleTryIt', + 'handleCancel' + ]); + + this.state = { + previewing: false + }; + } + handleTryIt () { + this.setState({previewing: true}); + this.props.onTryIt(); + } + handleCancel () { + window.history.back(); + } + supportedBrowser () { + if (platform.name === 'IE') { + return false; + } + return true; + } + render () { + return (this.supportedBrowser() ? + <PreviewModalComponent + previewing={this.state.previewing} + onCancel={this.handleCancel} + onTryIt={this.handleTryIt} + /> : + <BrowserModalComponent + onBack={this.handleCancel} + /> + ); + } +} + +PreviewModal.propTypes = { + onTryIt: PropTypes.func +}; + +const mapStateToProps = () => ({}); + +const mapDispatchToProps = dispatch => ({ + onTryIt: () => { + dispatch(closePreviewInfo()); + } +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(PreviewModal); diff --git a/src/css/typography.css b/src/css/typography.css new file mode 100644 index 0000000000000000000000000000000000000000..348e1df5a4aa0b6a9adac4b5f92a50e14ff9146b --- /dev/null +++ b/src/css/typography.css @@ -0,0 +1,13 @@ +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +h2 { + font-size: 1.5rem; + font-weight: bold; +} + +p { + font-size: 1rem; + line-height: 1.5em; +} diff --git a/src/reducers/modals.js b/src/reducers/modals.js index 55a55e4fea6a31e78334f6ee62aa45ec32d296a8..24e7269ecef206efd40118369a0c97c4f91b0fee 100644 --- a/src/reducers/modals.js +++ b/src/reducers/modals.js @@ -7,11 +7,14 @@ const MODAL_SOUND_LIBRARY = 'soundLibrary'; const MODAL_SPRITE_LIBRARY = 'spriteLibrary'; const MODAL_SOUND_RECORDER = 'soundRecorder'; const MODAL_EXTENSION_LIBRARY = 'extensionLibrary'; +const MODAL_PREVIEW_INFO = 'previewInfo'; + const initialState = { [MODAL_BACKDROP_LIBRARY]: false, [MODAL_COSTUME_LIBRARY]: false, [MODAL_EXTENSION_LIBRARY]: false, + [MODAL_PREVIEW_INFO]: true, [MODAL_SOUND_LIBRARY]: false, [MODAL_SPRITE_LIBRARY]: false, [MODAL_SOUND_RECORDER]: false @@ -62,6 +65,9 @@ const openSoundRecorder = function () { const openExtensionLibrary = function () { return openModal(MODAL_EXTENSION_LIBRARY); }; +const openPreviewInfo = function () { + return openModal(MODAL_PREVIEW_INFO); +}; const closeBackdropLibrary = function () { return closeModal(MODAL_BACKDROP_LIBRARY); }; @@ -71,6 +77,9 @@ const closeCostumeLibrary = function () { const closeExtensionLibrary = function () { return closeModal(MODAL_EXTENSION_LIBRARY); }; +const closePreviewInfo = function () { + return closeModal(MODAL_PREVIEW_INFO); +}; const closeSpriteLibrary = function () { return closeModal(MODAL_SPRITE_LIBRARY); }; @@ -85,12 +94,14 @@ export { openExtensionLibrary, openBackdropLibrary, openCostumeLibrary, + openPreviewInfo, openSoundLibrary, openSpriteLibrary, openSoundRecorder, closeBackdropLibrary, closeCostumeLibrary, closeExtensionLibrary, + closePreviewInfo, closeSpriteLibrary, closeSoundLibrary, closeSoundRecorder diff --git a/test/integration/test.js b/test/integration/test.js index aa616e5dcf9f4688241ece0d47418466e0268ec6..4653018e9780dd119a1330ecc5aca86d12e4d24e 100644 --- a/test/integration/test.js +++ b/test/integration/test.js @@ -37,6 +37,7 @@ describe('costumes, sounds and variables', () => { test('Blocks report when clicked in the toolbox', async () => { await loadUri(uri); + await clickXpath('//button[@title="tryit"]'); await clickText('Blocks'); await clickText('Operators', blocksTabScope); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation @@ -48,6 +49,7 @@ describe('costumes, sounds and variables', () => { test('Switching sprites updates the block menus', async () => { await loadUri(uri); + await clickXpath('//button[@title="tryit"]'); await clickText('Sound', blocksTabScope); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation // "Meow" sound block should be visible @@ -64,6 +66,7 @@ describe('costumes, sounds and variables', () => { test('Adding a costume', async () => { await loadUri(uri); + await clickXpath('//button[@title="tryit"]'); await clickText('Costumes'); await clickText('Add Costume'); const el = await findByXpath("//input[@placeholder='what are you looking for?']"); @@ -76,6 +79,7 @@ describe('costumes, sounds and variables', () => { test('Adding a backdrop', async () => { await loadUri(uri); + await clickXpath('//button[@title="tryit"]'); await clickText('Add Backdrop'); const el = await findByXpath("//input[@placeholder='what are you looking for?']"); await el.sendKeys('blue'); @@ -86,6 +90,7 @@ describe('costumes, sounds and variables', () => { test('Adding a sound', async () => { await loadUri(uri); + await clickXpath('//button[@title="tryit"]'); await clickText('Sounds'); // Delete the sound @@ -142,6 +147,7 @@ describe('costumes, sounds and variables', () => { .setSize(1920, 1080); const projectId = '96708228'; await loadUri(`${uri}#${projectId}`); + await clickXpath('//button[@title="tryit"]'); await new Promise(resolve => setTimeout(resolve, 2000)); await clickXpath('//img[@title="Full Screen Control"]'); await clickXpath('//img[@title="Go"]'); @@ -158,6 +164,7 @@ describe('costumes, sounds and variables', () => { test('Creating variables', async () => { await loadUri(uri); + await clickXpath('//button[@title="tryit"]'); await clickText('Blocks'); await clickText('Variables', blocksTabScope); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation @@ -187,6 +194,7 @@ describe('costumes, sounds and variables', () => { test('Importing extensions', async () => { await loadUri(uri); + await clickXpath('//button[@title="tryit"]'); await clickText('Blocks'); await clickText('Extensions'); await clickText('Pen', modalScope); // Modal closes @@ -211,6 +219,7 @@ describe('costumes, sounds and variables', () => { test('Deleting only sprite does not crash', async () => { const spriteTileContext = '*[starts-with(@class,"react-contextmenu-wrapper")]'; await loadUri(uri); + await clickXpath('//button[@title="tryit"]'); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation await rightClickText('Sprite1', spriteTileContext); await clickText('delete', spriteTileContext); @@ -224,6 +233,7 @@ describe('costumes, sounds and variables', () => { test('Custom procedures', async () => { await loadUri(uri); + await clickXpath('//button[@title="tryit"]'); await clickText('My Blocks'); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation await clickText('Make a Block'); @@ -244,6 +254,7 @@ describe('costumes, sounds and variables', () => { // "Coming Soon" test.skip('Localization', async () => { await loadUri(uri); + await clickXpath('//button[@title="tryit"]'); await clickText('Blocks'); await clickText('Extensions'); await clickText('Pen', modalScope); // Modal closes