diff --git a/package.json b/package.json index 645ee73e4..3ed372dc9 100644 --- a/package.json +++ b/package.json @@ -42,11 +42,11 @@ "copy-webpack-plugin": "^4.0.1", "css-loader": "^0.28.7", "enzyme": "^3.1.0", - "enzyme-adapter-react-16": "1.0.4", + "enzyme-adapter-react-16": "1.1.0", "eslint": "^4.7.1", "eslint-config-scratch": "^5.0.0", "eslint-plugin-import": "^2.7.0", - "eslint-plugin-react": "^7.2.1", + "eslint-plugin-react": "^7.5.1", "file-loader": "1.1.5", "get-float-time-domain-data": "0.1.0", "gh-pages": "github:rschamp/gh-pages#publish-branch-to-subfolder", @@ -67,9 +67,9 @@ "prop-types": "^15.5.10", "raf": "^3.4.0", "raw-loader": "0.5.1", - "react": "16.1.0", + "react": "16.1.1", "react-contextmenu": "2.8.0", - "react-dom": "16.1.0", + "react-dom": "16.1.1", "react-draggable": "3.0.3", "react-intl": "2.4.0", "react-intl-redux": "0.7.0", @@ -77,7 +77,7 @@ "react-redux": "5.0.6", "react-responsive": "3.0.0", "react-style-proptype": "3.1.0", - "react-tabs": "2.1.0", + "react-tabs": "2.1.1", "react-test-renderer": "16.1.1", "redux": "3.7.0", "redux-mock-store": "^1.2.3", diff --git a/src/components/asset-panel/selector.css b/src/components/asset-panel/selector.css index 2a4de1a25..63e686832 100644 --- a/src/components/asset-panel/selector.css +++ b/src/components/asset-panel/selector.css @@ -1,4 +1,5 @@ @import "../../css/colors.css"; +@import "../../css/units.css"; .wrapper { width: 150px; @@ -51,3 +52,14 @@ min-height: 5rem; margin: 1rem auto; } + +@media only screen and (max-width: $full-size-paint) { + .wrapper { + width: 80px; + } + + .list-item { + width: 4rem; + min-height: 4rem; + } +} diff --git a/src/components/close-button/close-button.jsx b/src/components/close-button/close-button.jsx index 416adec8e..e90bd1d64 100644 --- a/src/components/close-button/close-button.jsx +++ b/src/components/close-button/close-button.jsx @@ -7,6 +7,7 @@ import closeIcon from './icon--close.svg'; const CloseButton = props => (
( [styles.large]: props.size === CloseButton.SIZE_LARGE } )} + role="button" onClick={props.onClick} > (
@@ -76,6 +77,7 @@ class SpriteInfo extends React.Component {
@@ -116,6 +118,7 @@ class SpriteInfo extends React.Component { )} tabIndex="4" onClick={this.props.onClickVisible} + onKeyPress={this.props.onPressVisible} > blockInfo.json)); const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML(); - const toolboxXML = makeToolboxXML(dynamicBlocksXML); - this.props.onExtensionAdded(toolboxXML); + const toolboxXML = makeToolboxXML(this.props.vm.editingTarget.id, dynamicBlocksXML); + this.props.updateToolboxState(toolboxXML); } handleCategorySelected (categoryName) { this.workspace.toolbox_.setSelectedCategoryByName(categoryName); @@ -220,7 +227,7 @@ class Blocks extends React.Component { vm, isVisible, onActivateColorPicker, - onExtensionAdded, + updateToolboxState, onRequestCloseExtensionLibrary, toolboxXML, ...props @@ -257,7 +264,6 @@ Blocks.propTypes = { extensionLibraryVisible: PropTypes.bool, isVisible: PropTypes.bool, onActivateColorPicker: PropTypes.func, - onExtensionAdded: PropTypes.func, onRequestCloseExtensionLibrary: PropTypes.func, options: PropTypes.shape({ media: PropTypes.string, @@ -281,6 +287,7 @@ Blocks.propTypes = { comments: PropTypes.bool }), toolboxXML: PropTypes.string, + updateToolboxState: PropTypes.func, vm: PropTypes.instanceOf(VM).isRequired }; @@ -322,11 +329,11 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => ({ onActivateColorPicker: callback => dispatch(activateColorPicker(callback)), - onExtensionAdded: toolboxXML => { - dispatch(updateToolbox(toolboxXML)); - }, onRequestCloseExtensionLibrary: () => { dispatch(closeExtensionLibrary()); + }, + updateToolboxState: toolboxXML => { + dispatch(updateToolbox(toolboxXML)); } }); diff --git a/src/containers/sound-tab.jsx b/src/containers/sound-tab.jsx index 1984923d5..6143175dd 100644 --- a/src/containers/sound-tab.jsx +++ b/src/containers/sound-tab.jsx @@ -52,20 +52,18 @@ class SoundTab extends React.Component { render () { const { - editingTarget, - sprites, - stage, + vm, onNewSoundFromLibraryClick, onNewSoundFromRecordingClick } = this.props; - const target = editingTarget && sprites[editingTarget] ? sprites[editingTarget] : stage; - - if (!target) { + if (!vm.editingTarget) { return null; } - const sounds = target.sounds ? target.sounds.map(sound => ( + const sprite = vm.editingTarget.sprite; + + const sounds = sprite.sounds ? sprite.sounds.map(sound => ( { url: soundIcon, name: sound.name @@ -106,7 +104,7 @@ class SoundTab extends React.Component { onDeleteClick={this.handleDeleteSound} onItemClick={this.handleSelectSound} > - {editingTarget && target.sounds && target.sounds.length > 0 ? ( + {sprite.sounds && sprite.sounds.length > 0 ? ( ) : null} {this.props.soundRecorderVisible ? ( diff --git a/src/containers/sprite-info.jsx b/src/containers/sprite-info.jsx index 532fb37e5..f4b5ca696 100644 --- a/src/containers/sprite-info.jsx +++ b/src/containers/sprite-info.jsx @@ -10,7 +10,9 @@ class SpriteInfo extends React.Component { bindAll(this, [ 'handleChangeRotationStyle', 'handleClickVisible', - 'handleClickNotVisible' + 'handleClickNotVisible', + 'handlePressVisible', + 'handlePressNotVisible' ]); } handleChangeRotationStyle (e) { @@ -24,6 +26,18 @@ class SpriteInfo extends React.Component { e.preventDefault(); this.props.onChangeVisibility(false); } + handlePressVisible (e) { + if (e.key === ' ' || e.key === 'Enter') { + e.preventDefault(); + this.props.onChangeVisibility(true); + } + } + handlePressNotVisible (e) { + if (e.key === ' ' || e.key === 'Enter') { + e.preventDefault(); + this.props.onChangeVisibility(false); + } + } render () { return ( ); } diff --git a/src/css/units.css b/src/css/units.css index 3de6174a2..3e7f73f36 100644 --- a/src/css/units.css +++ b/src/css/units.css @@ -9,3 +9,7 @@ $stage-menu-height: 2.75rem; $library-header-height: 4.375rem; $form-radius: calc($space / 2); + +/* layout contants from `layout-constants.js` */ +$full-size: 1095px; +$full-size-paint: 1249px; diff --git a/src/index.jsx b/src/index.jsx index 6d301f516..e55fc0ef4 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -7,6 +7,11 @@ import ProjectLoaderHOC from './lib/project-loader-hoc.jsx'; import styles from './index.css'; +if (process.env.NODE_ENV === 'production' && typeof window === 'object') { + // Warn before navigating away + window.onbeforeunload = () => true; +} + const App = AppStateHOC(ProjectLoaderHOC(GUI)); const appTarget = document.createElement('div'); diff --git a/src/lib/layout-constants.js b/src/lib/layout-constants.js index 672c8013f..67c2ae44c 100644 --- a/src/lib/layout-constants.js +++ b/src/lib/layout-constants.js @@ -3,5 +3,6 @@ export default { fullStageHeight: 360, smallerStageWidth: 480 * 0.85, smallerStageHeight: 360 * 0.85, - fullSizeMinWidth: 1096 + fullSizeMinWidth: 1096, + fullSizePaintMinWidth: 1250 }; diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js index 315e1388c..608cf92b3 100644 --- a/src/lib/make-toolbox-xml.js +++ b/src/lib/make-toolbox-xml.js @@ -2,7 +2,8 @@ const categorySeparator = ''; const blockSeparator = ''; // At default scale, about 28px -const motion = ` +const motion = function (targetId) { + return ` @@ -120,14 +121,16 @@ const motion = ` ${blockSeparator} ${blockSeparator} - - - + + + ${categorySeparator} -`; + `; +}; -const looks = ` +const looks = function (targetId) { + return ` @@ -229,15 +232,17 @@ const looks = ` ${blockSeparator} - + - + ${categorySeparator} -`; + `; +}; -const sound = ` +const sound = function () { + return ` @@ -284,9 +289,11 @@ const sound = ` ${categorySeparator} -`; + `; +}; -const events = ` +const events = function () { + return ` @@ -317,9 +324,11 @@ const events = ` ${categorySeparator} -`; + `; +}; -const control = ` +const control = function () { + return ` @@ -354,9 +363,11 @@ const control = ` ${categorySeparator} -`; + `; +}; -const sensing = ` +const sensing = function () { + return ` @@ -422,9 +433,11 @@ const sensing = ` ${categorySeparator} -`; + `; +}; -const operators = ` +const operators = function () { + return ` @@ -602,33 +615,37 @@ const operators = ` ${categorySeparator} -`; + `; +}; -const data = ` +const data = function () { + return ` -`; + `; +}; const xmlOpen = ''; const xmlClose = ''; /** + * @param {!string} targetId - The current editing target * @param {string?} categoriesXML - null for default toolbox, or an XML string with elements. * @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox. */ -const makeToolboxXML = function (categoriesXML) { +const makeToolboxXML = function (targetId, categoriesXML) { const gap = [categorySeparator]; const everything = [ xmlOpen, - motion, gap, - looks, gap, - sound, gap, - events, gap, - control, gap, - sensing, gap, - operators, gap, - data + motion(targetId), gap, + looks(targetId), gap, + sound(targetId), gap, + events(targetId), gap, + control(targetId), gap, + sensing(targetId), gap, + operators(targetId), gap, + data(targetId) ]; if (categoriesXML) { diff --git a/src/reducers/toolbox.js b/src/reducers/toolbox.js index 4b06a807c..3f559884f 100644 --- a/src/reducers/toolbox.js +++ b/src/reducers/toolbox.js @@ -1,9 +1,9 @@ const UPDATE_TOOLBOX = 'scratch-gui/toolbox/UPDATE_TOOLBOX'; - import makeToolboxXML from '../lib/make-toolbox-xml'; const initialState = { - toolboxXML: makeToolboxXML() + // @todo switch this to make the stage's XML + toolboxXML: makeToolboxXML('') }; const reducer = function (state, action) { diff --git a/test/helpers/selenium-helper.js b/test/helpers/selenium-helper.js index c4f36ca05..ce3f04fbf 100644 --- a/test/helpers/selenium-helper.js +++ b/test/helpers/selenium-helper.js @@ -4,7 +4,7 @@ import bindAll from 'lodash.bindall'; import 'chromedriver'; // register path import webdriver from 'selenium-webdriver'; -const {By, until} = webdriver; +const {By, until, Button} = webdriver; class SeleniumHelper { constructor () { @@ -15,7 +15,9 @@ class SeleniumHelper { 'findByText', 'findByXpath', 'getDriver', - 'getLogs' + 'getLogs', + 'loadUri', + 'rightClickText' ]); } @@ -34,6 +36,14 @@ class SeleniumHelper { return this.findByXpath(`//body//${scope || '*'}//*[contains(text(), '${text}')]`); } + loadUri (uri) { + return this.driver + .get(`file://${uri}`) + .then(() => ( + this.driver.executeScript('window.onbeforeunload = undefined;') + )); + } + clickXpath (xpath) { return this.findByXpath(xpath).then(el => el.click()); } @@ -42,6 +52,12 @@ class SeleniumHelper { return this.findByText(text, scope).then(el => el.click()); } + rightClickText (text, scope) { + return this.findByText(text, scope).then(el => this.driver.actions() + .click(el, Button.RIGHT) + .perform()); + } + clickButton (text) { return this.clickXpath(`//button[contains(text(), '${text}')]`); } diff --git a/test/integration/examples.test.js b/test/integration/examples.test.js index 7fffa903a..908dbfd36 100644 --- a/test/integration/examples.test.js +++ b/test/integration/examples.test.js @@ -9,7 +9,8 @@ const { clickXpath, findByXpath, getDriver, - getLogs + getLogs, + loadUri } = new SeleniumHelper(); const errorWhitelist = [ @@ -31,7 +32,7 @@ describe('player example', () => { test('Load a project by ID', async () => { const projectId = '96708228'; - await driver.get(`file://${uri}#${projectId}`); + await loadUri(`${uri}#${projectId}`); await new Promise(resolve => setTimeout(resolve, 2000)); await clickXpath('//img[@title="Go"]'); await new Promise(resolve => setTimeout(resolve, 2000)); @@ -54,7 +55,7 @@ describe('blocks example', () => { test('Load a project by ID', async () => { const projectId = '96708228'; - await driver.get(`file://${uri}#${projectId}`); + await loadUri(`${uri}#${projectId}`); await new Promise(resolve => setTimeout(resolve, 2000)); await clickXpath('//img[@title="Go"]'); await new Promise(resolve => setTimeout(resolve, 2000)); @@ -64,7 +65,7 @@ describe('blocks example', () => { }); test('Change categories', async () => { - await driver.get(`file://${uri}`); + await loadUri(`${uri}`); await clickText('Looks'); await clickText('Sound'); await clickText('Events'); diff --git a/test/integration/test.js b/test/integration/test.js index 86729cf27..cfea4e217 100644 --- a/test/integration/test.js +++ b/test/integration/test.js @@ -8,7 +8,9 @@ const { findByText, findByXpath, getDriver, - getLogs + getLogs, + loadUri, + rightClickText } = new SeleniumHelper(); const uri = path.resolve(__dirname, '../../build/index.html'); @@ -34,9 +36,8 @@ describe('costumes, sounds and variables', () => { await driver.quit(); }); - test('Blocks report when clicked in the toolbox', async () => { - await driver.get(`file://${uri}`); + await loadUri(uri); await clickText('Blocks'); await clickText('Operators', blocksTabScope); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation @@ -47,7 +48,7 @@ describe('costumes, sounds and variables', () => { }); test('Switching sprites updates the block menus', async () => { - await driver.get(`file://${uri}`); + await loadUri(uri); await clickText('Sound', blocksTabScope); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation // "meow" sound block should be visible @@ -60,7 +61,7 @@ describe('costumes, sounds and variables', () => { }); test('Adding a costume', async () => { - await driver.get(`file://${uri}`); + await loadUri(uri); await clickText('Costumes'); await clickText('Add Costume'); const el = await findByXpath("//input[@placeholder='what are you looking for?']"); @@ -73,17 +74,22 @@ describe('costumes, sounds and variables', () => { }); test('Adding a sound', async () => { - await driver.get(`file://${uri}`); + await loadUri(uri); await clickText('Sounds'); + + // Delete the sound + await rightClickText('meow', soundsTabScope); + await clickText('delete', soundsTabScope); + await driver.switchTo().alert() + .accept(); + + // Add a sound await clickText('Add Sound'); const el = await findByXpath("//input[@placeholder='what are you looking for?']"); await el.sendKeys('chom'); await clickText('chomp'); // Should close the modal, then click the sounds in the selector - await clickText('meow', soundsTabScope); await clickText('chomp', soundsTabScope); await clickXpath('//button[@title="Play"]'); - await clickText('meow', soundsTabScope); - await clickXpath('//button[@title="Play"]'); await clickText('Louder'); await clickText('Softer'); @@ -99,7 +105,7 @@ describe('costumes, sounds and variables', () => { test('Load a project by ID', async () => { const projectId = '96708228'; - await driver.get(`file://${uri}#${projectId}`); + await loadUri(`${uri}#${projectId}`); await new Promise(resolve => setTimeout(resolve, 2000)); await clickXpath('//img[@title="Go"]'); await new Promise(resolve => setTimeout(resolve, 2000)); @@ -109,7 +115,7 @@ describe('costumes, sounds and variables', () => { }); test('Creating variables', async () => { - await driver.get(`file://${uri}`); + await loadUri(uri); await clickText('Blocks'); await clickText('Data', blocksTabScope); await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation @@ -133,7 +139,7 @@ describe('costumes, sounds and variables', () => { }); test('Importing extensions', async () => { - await driver.get(`file://${uri}`); + await loadUri(uri); await clickText('Blocks'); await clickText('Extensions'); await clickText('Pen', modalScope); // Modal closes diff --git a/test/unit/components/__snapshots__/icon-button.test.jsx.snap b/test/unit/components/__snapshots__/icon-button.test.jsx.snap index 3a26f0acf..b5f3e7a10 100644 --- a/test/unit/components/__snapshots__/icon-button.test.jsx.snap +++ b/test/unit/components/__snapshots__/icon-button.test.jsx.snap @@ -4,6 +4,7 @@ exports[`IconButtonComponent matches snapshot 1`] = `