diff --git a/.babelrc b/.babelrc index 8f910cdf7a6..aacea1f975e 100644 --- a/.babelrc +++ b/.babelrc @@ -1,10 +1,13 @@ { "plugins": [ - "syntax-dynamic-import", - "transform-async-to-generator", - "transform-object-rest-spread", + "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-transform-async-to-generator", + "@babel/plugin-proposal-object-rest-spread", ["react-intl", { "messagesDir": "./translations/messages/" }]], - "presets": [["env", {"targets": {"browsers": ["last 3 versions", "Safari >= 8", "iOS >= 8"]}}], "react"] + "presets": [ + ["@babel/preset-env", {"targets": {"browsers": ["last 3 versions", "Safari >= 8", "iOS >= 8"]}}], + "@babel/preset-react" + ] } diff --git a/.gitattributes b/.gitattributes index 84e89375a72..e81c38bb515 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,6 +6,11 @@ # File types which we know are binary +# Treat SVG files as binary so that their contents don't change due to line +# endings. The contents of SVGs must not change from the way they're stored +# on assets.scratch.mit.edu so that MD5 calculations don't change. +*.svg binary + # Prefer LF for most file types *.frag text eol=lf *.htm text eol=lf diff --git a/.gitignore b/.gitignore index 0b8a0db40e6..de7972e5eed 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ # NPM /node_modules npm-* +/package-lock.json # Testing /.nyc_output diff --git a/.npmignore b/.npmignore index 5d69158021a..61247064a40 100644 --- a/.npmignore +++ b/.npmignore @@ -8,9 +8,11 @@ npm-* # Testing /.nyc_output /coverage +/test # Build /.opt-in +/build # generated translation files /translations diff --git a/.travis.yml b/.travis.yml index 5805f6b6839..9290080d2f3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,21 +29,13 @@ before_deploy: export BEFORE_DEPLOY_RAN=true fi deploy: -- provider: script - on: - all_branches: true - skip_cleanup: true - script: npm run deploy -- -x -e $TRAVIS_BRANCH -r https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git -- provider: script - on: - all_branches: true - script: npm run prune -- https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git - provider: npm on: branch: - master - develop - smoke + condition: $TRAVIS_EVENT_TYPE != cron skip_cleanup: true email: $NPM_EMAIL api_key: $NPM_TOKEN @@ -53,11 +45,29 @@ deploy: branch: - master - $PREVIEW_BRANCH + condition: $TRAVIS_EVENT_TYPE != cron access_key_id: $AWS_ACCESS_KEY_ID secret_access_key: $AWS_SECRET_ACCESS_KEY bucket: $AWS_BUCKET_NAME acl: public_read skip_cleanup: true local_dir: build +- provider: script + on: + all_branches: true + condition: $TRAVIS_EVENT_TYPE != cron + skip_cleanup: true + script: npm run deploy -- -x -e $TRAVIS_BRANCH -r https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git +- provider: script + on: + all_branches: true + condition: $TRAVIS_EVENT_TYPE != cron + script: npm run prune -- https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git +- provider: script + on: + branch: develop + condition: $TRAVIS_EVENT_TYPE == cron + skip_cleanup: true + script: npm run i18n:src && npm run i18n:push after_deploy: - 'curl -X POST -H "Fastly-Key: $FASTLY_TOKEN" -H "Accept: application/json" https://api.fastly.com/service/$FASTLY_SERVICE_ID/purge_all' diff --git a/README.md b/README.md index c036a170930..440a3a4b06a 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ npm install https://github.com/LLK/scratch-gui.git ``` If you want to edit/play yourself: ```bash -git clone git@github.com:LLK/scratch-gui.git +git clone https://github.com/LLK/scratch-gui.git cd scratch-gui npm install ``` @@ -28,33 +28,100 @@ npm start ``` Then go to [http://localhost:8601/](http://localhost:8601/) - the playground outputs the default GUI component +## Developing alongside other Scratch repositories + +### Linking this code to another project's `node_modules/scratch-gui` + +#### Configuration + +If you wish to develop scratch-gui alongside other scratch repositories that depend on it, you may wish +to have the other repositories use your local scratch-gui build instead of fetching the current production +version of the scratch-gui that is found by default using `npm install`. + +To do this: +1. Make sure you have run `npm install` from this (scratch-gui) repository's top level +2. Make sure you have run `npm install` from the top level of each repository (such as scratch-www) that depends on scratch-gui +3. From this (scratch-gui) repository's top level, build the `dist` directory by running `BUILD_MODE=dist npm run build` +4. From this (scratch-gui) repository's top level, establish a link to this repository by running `npm link` +5. From the top level of each repository that depends on scratch-gui, run `npm link scratch-gui` +6. Build or run the repositories that depend on scratch-gui + +Instead of `BUILD_MODE=dist npm run build` you can also use `BUILD_MODE=dist npm run watch`, however this may be unreliable. + +#### Oh no! It didn't work! +* Follow the recipe above step by step and don't change the order. It is especially important to run npm first because installing after the linking will reset the linking. +* Make sure the repositories are siblings on your machine's file tree. +* If you have multiple Terminal tabs or windows open for the different Scratch repositories, make sure to use the same node version in all of them. +* In the worst case unlink the repositories by running `npm unlink` in both, and start over. + ## Testing -NOTE: If you're a windows user, please run these scripts in Windows `cmd.exe` instead of Git Bash/MINGW64. +### Documentation + +You may want to review the documentation for [Jest](https://facebook.github.io/jest/docs/en/api.html) and [Enzyme](http://airbnb.io/enzyme/docs/api/) as you write your tests. -Run linter, unit tests, build, and integration tests. +See [jest cli docs](https://facebook.github.io/jest/docs/en/cli.html#content) for more options. + +### Running tests + +*NOTE: If you're a windows user, please run these scripts in Windows `cmd.exe` instead of Git Bash/MINGW64.* + +Before running any test, make sure you have run `npm install` from this (scratch-gui) repository's top level. + +#### Main testing command + +To run linter, unit tests, build, and integration tests, all at once: ```bash npm test ``` -Run unit tests in isolation. +#### Running unit tests + +To run unit tests in isolation: ```bash -npm run unit-test +npm run test:unit ``` -Run unit tests in watch mode (watches for code changes and continuously runs tests). See [jest cli docs](https://facebook.github.io/jest/docs/en/cli.html#content) for more options. +To run unit tests in watch mode (watches for code changes and continuously runs tests): ```bash -npm run unit-test -- --watch +npm run test:unit -- --watch ``` -Run integration tests in isolation. +You can run a single file of integration tests (in this example, the `button` tests): + ```bash -npm run integration-test +$(npm bin)/jest --runInBand test/unit/components/button.test.jsx ``` -You may want to review the documentation for [Jest](https://facebook.github.io/jest/docs/en/api.html) and [Enzyme](http://airbnb.io/enzyme/docs/api/) as you write your tests. +#### Running integration tests -## Publishing to GitHub Pages +Integration tests use a headless browser to manipulate the actual html and javascript that the repo +produces. You will not see this activity (though you can hear it when sounds are played!). +Note that integration tests require you to first create a build that can be loaded in a browser: + +```bash +npm run build +``` + +Then, you can run all integration tests: + +```bash +npm run test:integration +``` + +Or, you can run a single file of integration tests (in this example, the `backpack` tests): + +```bash +$(npm bin)/jest --runInBand test/integration/backpack.test.js +``` + +If you want to watch the browser as it runs the test, rather than running headless, use: + +```bash +USE_HEADLESS=no $(npm bin)/jest --runInBand test/integration/backpack.test.js +``` + +## Publishing to GitHub Pages You can publish the GUI to github.io so that others on the Internet can view it. [Read the wiki for a step-by-step guide.](https://github.com/LLK/scratch-gui/wiki/Publishing-to-GitHub-Pages) diff --git a/package.json b/package.json index 1b589d22840..fa8bb61f44a 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "clean": "rimraf ./build && mkdirp build && rimraf ./dist && mkdirp dist", "deploy": "touch build/.nojekyll && gh-pages -t -d build -m \"Build for $(git log --pretty=format:%H -n1)\"", "prune": "./prune-gh-pages.sh", - "i18n:src": "babel src > tmp.js && rimraf tmp.js && build-i18n-src ./translations/messages/src ./translations/", + "i18n:push": "tx-push-src scratch-editor interface translations/en.json", + "i18n:src": "rimraf ./translations/messages/src && babel src > tmp.js && rimraf tmp.js && build-i18n-src ./translations/messages/src ./translations/ && npm run i18n:push", "start": "webpack-dev-server", "test": "npm run test:lint && npm run test:unit && npm run build && npm run test:integration", "test:integration": "jest --runInBand test[\\\\/]integration", @@ -29,31 +30,34 @@ "react-dom": "^16.0.0" }, "devDependencies": { - "arraybuffer-loader": "^1.0.3", - "autoprefixer": "^8.1.0", - "babel-core": "^6.23.1", - "babel-eslint": "^8.0.1", - "babel-loader": "^7.1.0", - "babel-plugin-syntax-dynamic-import": "^6.18.0", - "babel-plugin-transform-async-to-generator": "^6.24.1", - "babel-plugin-transform-object-rest-spread": "^6.22.0", - "babel-preset-env": "^1.6.1", - "babel-preset-react": "^6.22.0", + "@babel/cli": "^7.1.2", + "@babel/core": "^7.1.2", + "@babel/plugin-proposal-object-rest-spread": "^7.0.0", + "@babel/plugin-syntax-dynamic-import": "^7.0.0", + "@babel/plugin-transform-async-to-generator": "^7.1.0", + "@babel/preset-env": "^7.1.0", + "@babel/preset-react": "^7.0.0", + "arraybuffer-loader": "^1.0.6", + "autoprefixer": "^9.0.1", + "babel-core": "7.0.0-bridge.0", + "babel-eslint": "^10.0.1", + "babel-loader": "^8.0.4", "base64-loader": "1.0.0", - "bowser": "1.9.3", - "chromedriver": "2.40.0", + "bowser": "1.9.4", + "chromedriver": "2.44.1", "classnames": "2.2.6", + "computed-style-to-inline-style": "3.0.0", "copy-webpack-plugin": "^4.5.1", "core-js": "2.5.7", - "css-loader": "^0.28.11", - "enzyme": "^3.1.0", - "enzyme-adapter-react-16": "1.1.1", + "css-loader": "^1.0.0", + "enzyme": "^3.5.0", + "enzyme-adapter-react-16": "1.3.0", "es6-object-assign": "1.1.0", "eslint": "^5.0.1", "eslint-config-scratch": "^5.0.0", "eslint-plugin-import": "^2.8.0", - "eslint-plugin-react": "^7.5.1", - "file-loader": "1.1.11", + "eslint-plugin-react": "7.11.1", + "file-loader": "2.0.0", "get-float-time-domain-data": "0.1.0", "get-user-media-promise": "1.1.4", "gh-pages": "github:rschamp/gh-pages#publish-branch-to-subfolder", @@ -61,6 +65,7 @@ "immutable": "3.8.2", "intl": "1.2.5", "jest": "^21.0.0", + "js-base64": "2.4.9", "keymirror": "0.1.1", "lodash.bindall": "4.4.0", "lodash.debounce": "4.0.8", @@ -68,52 +73,55 @@ "lodash.isequal": "4.5.0", "lodash.omit": "4.5.0", "lodash.pick": "4.4.0", + "lodash.throttle": "4.0.1", "minilog": "3.1.0", "mkdirp": "^0.5.1", - "postcss-import": "^11.0.0", - "postcss-loader": "^2.1.4", - "postcss-simple-vars": "^4.0.0", + "papaparse": "4.6.2", + "postcss-import": "^12.0.0", + "postcss-loader": "^3.0.0", + "postcss-simple-vars": "^5.0.1", "prop-types": "^15.5.10", + "query-string": "^5.1.1", "raf": "^3.4.0", "raw-loader": "^0.5.1", "react": "16.2.0", - "react-contextmenu": "2.9.2", + "react-contextmenu": "2.9.4", "react-dom": "16.2.0", "react-draggable": "3.0.5", "react-ga": "2.5.3", "react-intl": "2.4.0", - "react-modal": "3.4.4", - "react-popover": "0.5.7", + "react-modal": "3.6.1", + "react-popover": "0.5.10", "react-redux": "5.0.7", - "react-responsive": "4.1.0", - "react-style-proptype": "3.2.1", - "react-tabs": "2.2.2", + "react-responsive": "5.0.0", + "react-style-proptype": "3.2.2", + "react-tabs": "2.3.0", "react-test-renderer": "16.2.0", - "react-tooltip": "3.6.1", - "react-virtualized": "9.20.0", + "react-tooltip": "3.8.0", + "react-virtualized": "9.20.1", "redux": "3.7.2", "redux-mock-store": "^1.2.3", "redux-throttle": "0.1.1", "rimraf": "^2.6.1", - "scratch-audio": "0.1.0-prerelease.20180625202813", - "scratch-blocks": "0.1.0-prerelease.1531482946", - "scratch-l10n": "3.0.20180712200642", - "scratch-paint": "0.2.0-prerelease.20180712195436", - "scratch-render": "0.1.0-prerelease.20180618173030", - "scratch-storage": "0.5.1", - "scratch-svg-renderer": "0.2.0-prerelease.20180712223402", - "scratch-vm": "0.1.0-prerelease.1531486395", + "scratch-audio": "0.1.0-prerelease.20190114210212", + "scratch-blocks": "0.1.0-prerelease.1547735159", + "scratch-l10n": "3.1.20190117191816", + "scratch-paint": "0.2.0-prerelease.20190114205252", + "scratch-render": "0.1.0-prerelease.20190116202853", + "scratch-storage": "1.2.2", + "scratch-svg-renderer": "0.2.0-prerelease.20190110205335", + "scratch-vm": "0.2.0-prerelease.20190116202234", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", - "style-loader": "^0.21.0", + "style-loader": "^0.23.0", "svg-to-image": "1.1.3", - "text-encoding": "0.6.4", + "text-encoding": "0.7.0", "to-style": "1.3.3", "uglifyjs-webpack-plugin": "^1.2.5", "wav-encoder": "1.3.0", "web-audio-test-api": "^0.5.2", "webpack": "^4.6.0", - "webpack-cli": "^2.0.15", + "webpack-cli": "^3.1.0", "webpack-dev-server": "^3.1.3", "xhr": "2.5.0" }, diff --git a/src/.eslintrc.js b/src/.eslintrc.js index 14b082cdfe7..4b05b2d2747 100644 --- a/src/.eslintrc.js +++ b/src/.eslintrc.js @@ -12,7 +12,10 @@ module.exports = { 'import/no-commonjs': 'error', 'import/no-amd': 'error', 'import/no-nodejs-modules': 'error', - 'react/jsx-no-literals': 'error' + 'react/jsx-no-literals': 'error', + 'no-confusing-arrow': ['error', { + 'allowParens': true + }] }, settings: { react: { diff --git a/src/components/action-menu/action-menu.css b/src/components/action-menu/action-menu.css index 8f39946c128..14dbb5df031 100644 --- a/src/components/action-menu/action-menu.css +++ b/src/components/action-menu/action-menu.css @@ -1,4 +1,6 @@ @import "../../css/colors.css"; +@import "../../css/units.css"; +@import "../../css/z-index.css"; $main-button-size: 2.75rem; $more-button-size: 2.25rem; @@ -44,7 +46,7 @@ button::-moz-focus-inner { width: $main-button-size; height: $main-button-size; box-shadow: 0 0 0 4px $motion-transparent; - z-index: 20; /* TODO reorder layout to prevent z-index need */ + z-index: $z-index-add-button; transition: transform, box-shadow 0.5s; } @@ -58,6 +60,10 @@ button::-moz-focus-inner { height: calc($main-button-size - 1rem); } +[dir="rtl"] .main-icon { + transform: scaleX(-1); +} + .more-buttons-outer { /* Need to use two divs to set different overflow x/y @@ -71,6 +77,7 @@ button::-moz-focus-inner { border-top-right-radius: $more-button-size; width: $more-button-size; margin-left: calc(($main-button-size - $more-button-size) / 2); + margin-right: calc(($main-button-size - $more-button-size) / 2); position: absolute; bottom: calc($main-button-size); @@ -147,10 +154,10 @@ button::-moz-focus-inner { .tooltip { border: 1px solid hsla(0, 0%, 0%, .1) !important; - border-radius: .25rem !important; + border-radius: $form-radius !important; box-shadow: 0 0 .5rem hsla(0, 0%, 0%, .25) !important; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important; - z-index: 100 !important; + z-index: $z-index-tooltip !important; } $arrow-size: 0.5rem; diff --git a/src/components/action-menu/action-menu.jsx b/src/components/action-menu/action-menu.jsx index 2725c502b42..36cf5d4449f 100644 --- a/src/components/action-menu/action-menu.jsx +++ b/src/components/action-menu/action-menu.jsx @@ -24,6 +24,7 @@ class ActionMenu extends React.Component { isOpen: false, forceHide: false }; + this.mainTooltipId = `tooltip-${Math.random()}`; } componentDidMount () { // Touch start on the main button is caught to trigger open and not click @@ -77,6 +78,9 @@ class ActionMenu extends React.Component { return event => { ReactTooltip.hide(); if (fn) fn(event); + // Blur the button so it does not keep focus after being clicked + // This prevents keyboard events from triggering the button + this.buttonRef.blur(); this.setState({forceHide: true, isOpen: false}, () => { setTimeout(() => this.setState({forceHide: false})); }); @@ -101,11 +105,10 @@ class ActionMenu extends React.Component { img: mainImg, title: mainTitle, moreButtons, + tooltipPlace, onClick } = this.props; - const mainTooltipId = `tooltip-${Math.random()}`; - return (
- { /* eslint-disable max-len */ }
-
+ { /* eslint-disable max-len */ }
+ {
+ props.error ?