diff --git a/.travis.yml b/.travis.yml index 5805f6b6839..ba21af32461 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,15 +29,6 @@ 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: @@ -59,5 +50,14 @@ deploy: acl: public_read skip_cleanup: true local_dir: build +- 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 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..66378014402 100644 --- a/README.md +++ b/README.md @@ -28,33 +28,89 @@ npm start ``` Then go to [http://localhost:8601/](http://localhost:8601/) - the playground outputs the default GUI component +## Developing alongside other Scratch repositories +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. + +See [jest cli docs](https://facebook.github.io/jest/docs/en/cli.html#content) for more options. -Run linter, unit tests, build, and integration tests. +### 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 test:unit +``` + +To run unit tests in watch mode (watches for code changes and continuously runs tests): ```bash -npm run unit-test +npm run test:unit -- --watch ``` -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. +#### Running integration tests + +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 unit-test -- --watch +npm run build ``` -Run integration tests in isolation. +Then, you can run all integration tests: + ```bash -npm run integration-test +npm run test:integration ``` -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. +Or, you can run a single file of integration tests (in this example, the `backpack` tests): -## Publishing to GitHub Pages +```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..620455b8ea7 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,8 @@ "react-dom": "^16.0.0" }, "devDependencies": { - "arraybuffer-loader": "^1.0.3", - "autoprefixer": "^8.1.0", + "arraybuffer-loader": "^1.0.6", + "autoprefixer": "^9.0.1", "babel-core": "^6.23.1", "babel-eslint": "^8.0.1", "babel-loader": "^7.1.0", @@ -40,14 +40,14 @@ "babel-preset-env": "^1.6.1", "babel-preset-react": "^6.22.0", "base64-loader": "1.0.0", - "bowser": "1.9.3", - "chromedriver": "2.40.0", + "bowser": "1.9.4", + "chromedriver": "2.42.0", "classnames": "2.2.6", "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", @@ -70,50 +70,50 @@ "lodash.pick": "4.4.0", "minilog": "3.1.0", "mkdirp": "^0.5.1", - "postcss-import": "^11.0.0", - "postcss-loader": "^2.1.4", - "postcss-simple-vars": "^4.0.0", + "postcss-import": "^12.0.0", + "postcss-loader": "^3.0.0", + "postcss-simple-vars": "^5.0.1", "prop-types": "^15.5.10", "raf": "^3.4.0", "raw-loader": "^0.5.1", "react": "16.2.0", - "react-contextmenu": "2.9.2", + "react-contextmenu": "2.9.3", "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-blocks": "0.1.0-prerelease.1538658492", + "scratch-l10n": "3.0.20181004141631", + "scratch-paint": "0.2.0-prerelease.20180926191006", + "scratch-render": "0.1.0-prerelease.20181002192350", + "scratch-storage": "1.0.3", + "scratch-svg-renderer": "0.2.0-prerelease.20180926143036", + "scratch-vm": "0.2.0-prerelease.20181001223744", "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..e82103ca10a 100644 --- a/src/components/action-menu/action-menu.css +++ b/src/components/action-menu/action-menu.css @@ -1,4 +1,5 @@ @import "../../css/colors.css"; +@import "../../css/z-index.css"; $main-button-size: 2.75rem; $more-button-size: 2.25rem; @@ -44,7 +45,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 +59,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 +76,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); @@ -150,7 +156,7 @@ button::-moz-focus-inner { border-radius: .25rem !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..01070b27f49 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 @@ -101,11 +102,10 @@ class ActionMenu extends React.Component { img: mainImg, title: mainTitle, moreButtons, + tooltipPlace, onClick } = this.props; - const mainTooltipId = `tooltip-${Math.random()}`; - return (
@@ -142,7 +142,7 @@ class ActionMenu extends React.Component { fileAccept, fileChange, fileInput}, keyId) => { const isComingSoon = !handleClick; const hasFileInput = fileInput; - const tooltipId = title; + const tooltipId = `${this.mainTooltipId}-${title}`; return (
); @@ -198,7 +198,8 @@ ActionMenu.propTypes = { fileInput: PropTypes.func // Optional, only for file upload })), onClick: PropTypes.func.isRequired, - title: PropTypes.node.isRequired + title: PropTypes.node.isRequired, + tooltipPlace: PropTypes.string }; export default ActionMenu; diff --git a/src/components/alerts/alert.css b/src/components/alerts/alert.css new file mode 100644 index 00000000000..c4f00a74191 --- /dev/null +++ b/src/components/alerts/alert.css @@ -0,0 +1,36 @@ +@import "../../css/units.css"; +@import "../../css/colors.css"; +@import "../../css/z-index.css"; + +.alert { + width: 100%; + background: #FFF0DF; + display: flex; + flex-direction: row; + overflow: hidden; + align-items: left; + border: 1px solid #FF8C1A; + border-radius: 8px; + padding: 8px; + box-shadow: 0px 0px 0px 2px rgba(255, 140, 26, 0.25); + margin-bottom: 7px; +} + +.alert-icon { + margin-right: 5px; + vertical-align: middle; +} + +.alert-message { + color: #555; + font-weight: bold; + font-size: 12px; + line-height: 22pt; + width: 100%; +} + +.alert-close-button { + margin-top: 7px; + margin-right: 4px; + outline-style:none; +} diff --git a/src/components/alerts/alert.jsx b/src/components/alerts/alert.jsx new file mode 100644 index 00000000000..d0f65309e08 --- /dev/null +++ b/src/components/alerts/alert.jsx @@ -0,0 +1,41 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Box from '../box/box.jsx'; +import CloseButton from '../close-button/close-button.jsx'; + +import styles from './alert.css'; + +const AlertComponent = ({ + iconURL, + message, + onCloseAlert +}) => ( + +
+ {iconURL ? ( + + ) : null} + {message} +
+ +
+); + +AlertComponent.propTypes = { + iconURL: PropTypes.string, + message: PropTypes.string, + onCloseAlert: PropTypes.func.isRequired +}; + +export default AlertComponent; diff --git a/src/components/alerts/alerts.jsx b/src/components/alerts/alerts.jsx new file mode 100644 index 00000000000..1835a50a820 --- /dev/null +++ b/src/components/alerts/alerts.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Box from '../box/box.jsx'; +import Alert from '../../containers/alert.jsx'; + +const AlertsComponent = ({ + alertsList, + className, + onCloseAlert +}) => ( + + {alertsList.map((a, index) => ( + + ))} + +); + +AlertsComponent.propTypes = { + alertsList: PropTypes.arrayOf(PropTypes.object), + className: PropTypes.string, + onCloseAlert: PropTypes.func +}; + +export default AlertsComponent; diff --git a/src/components/asset-panel/asset-panel.css b/src/components/asset-panel/asset-panel.css index 9a999057b57..bfd578909fd 100644 --- a/src/components/asset-panel/asset-panel.css +++ b/src/components/asset-panel/asset-panel.css @@ -5,16 +5,32 @@ display: flex; flex-grow: 1; border: 1px solid $ui-black-transparent; - border-top-right-radius: $space; background: white; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 0.85rem; } +[dir="ltr"] .wrapper { + border-top-right-radius: $space; + border-bottom-right-radius: $space; +} + +[dir="rtl"] .wrapper { + border-top-left-radius: $space; + border-bottom-left-radius: $space; +} + .detail-area { display: flex; flex-grow: 1; flex-shrink: 1; - border-left: 1px solid $ui-black-transparent; overflow-y: auto; } + +[dir="ltr"] .detail-area { + border-left: 1px solid $ui-black-transparent; +} + +[dir="rtl"] .detail-area { + border-right: 1px solid $ui-black-transparent; +} diff --git a/src/components/asset-panel/icon--sound-rtl.svg b/src/components/asset-panel/icon--sound-rtl.svg new file mode 100644 index 00000000000..adf85017394 --- /dev/null +++ b/src/components/asset-panel/icon--sound-rtl.svg @@ -0,0 +1,20 @@ + + + + Artboard + Created with Sketch. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/asset-panel/selector.css b/src/components/asset-panel/selector.css index 3dc4e803d9e..535c38b0be6 100644 --- a/src/components/asset-panel/selector.css +++ b/src/components/asset-panel/selector.css @@ -32,6 +32,7 @@ $fade-out-distance: 100px; position: absolute; bottom: 0; left: 0; + right:0; background: linear-gradient(rgba(232,237,241, 0),rgba(232,237,241, 1)); height: $fade-out-distance; width: 100%; @@ -49,6 +50,8 @@ $fade-out-distance: 100px; overflow-y: scroll; display: flex; flex-direction: column; + /* Make sure there is room to scroll beyond the last tile */ + padding-bottom: 70px; } .list-item { @@ -70,5 +73,5 @@ $fade-out-distance: 100px; .list-item.placeholder { background: black; - filter: opacity(15%) brightness(20%); + filter: opacity(15%) brightness(0%); } diff --git a/src/components/backpack/backpack.css b/src/components/backpack/backpack.css index 9143844d097..33585ca90de 100644 --- a/src/components/backpack/backpack.css +++ b/src/components/backpack/backpack.css @@ -9,7 +9,6 @@ .backpack-header { margin-top: 0.5rem; border: 1px solid $ui-black-transparent; - border-top-right-radius: $space; background: $ui-white; padding: 0.25rem; text-align: center; @@ -20,6 +19,14 @@ user-select: none; } +[dir="ltr"] .backpack-header { + border-top-right-radius: $space; +} + +[dir="rtl"] .backpack-header { + border-top-left-radius: $space; +} + .backpack-list { position: relative; display: flex; diff --git a/src/components/backpack/backpack.jsx b/src/components/backpack/backpack.jsx index 0f7b0b89f7e..04cdc293798 100644 --- a/src/components/backpack/backpack.jsx +++ b/src/components/backpack/backpack.jsx @@ -17,7 +17,7 @@ const dragTypeMap = { sprite: DragConstants.BACKPACK_SPRITE }; -const Backpack = ({contents, dragOver, dropAreaRef, error, expanded, loading, onToggle, onDelete}) => ( +const Backpack = ({containerRef, contents, dragOver, error, expanded, loading, onToggle, onDelete}) => (
{error ? (
@@ -104,6 +104,7 @@ const Backpack = ({contents, dragOver, dropAreaRef, error, expanded, loading, on ); Backpack.propTypes = { + containerRef: PropTypes.func, contents: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string, thumbnailUrl: PropTypes.string, @@ -111,7 +112,6 @@ Backpack.propTypes = { name: PropTypes.string })), dragOver: PropTypes.bool, - dropAreaRef: PropTypes.func, error: PropTypes.bool, expanded: PropTypes.bool, loading: PropTypes.bool, diff --git a/src/components/blocks/blocks.css b/src/components/blocks/blocks.css index e02486ce0fe..3d87405893c 100644 --- a/src/components/blocks/blocks.css +++ b/src/components/blocks/blocks.css @@ -12,6 +12,13 @@ border-bottom-right-radius: $space; } +[dir="rtl"] .blocks :global(.injectionDiv) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-top-left-radius: $space; + border-bottom-left-radius: $space; +} + .blocks :global(.blocklyMainBackground) { stroke: none; } @@ -31,6 +38,11 @@ -ms-overflow-style: none; } +[dir="rtl"] .blocks :global(.blocklyToolboxDiv) { + border-right: none; + border-left: 1px solid $ui-black-transparent; +} + .blocks :global(.blocklyToolboxDiv::-webkit-scrollbar) { display: none; } @@ -40,6 +52,12 @@ box-sizing: content-box; } +[dir="rtl"] .blocks :global(.blocklyFlyout) { + border-right: none; + border-left: 1px solid $ui-black-transparent; +} + + .blocks :global(.blocklyBlockDragSurface) { /* Fix an issue where the drag surface was preventing hover events for sharing blocks. diff --git a/src/components/browser-modal/browser-modal.css b/src/components/browser-modal/browser-modal.css index aa8840c4cc8..a7f86ac4efe 100644 --- a/src/components/browser-modal/browser-modal.css +++ b/src/components/browser-modal/browser-modal.css @@ -34,6 +34,10 @@ background-size: cover; } +[dir="rtl"] .illustration { + transform: scaleX(-1); +} + .body { background: $ui-white; padding: 1.5rem 2.25rem; diff --git a/src/components/browser-modal/browser-modal.jsx b/src/components/browser-modal/browser-modal.jsx index bb5c82fcffa..9afac0e3891 100644 --- a/src/components/browser-modal/browser-modal.jsx +++ b/src/components/browser-modal/browser-modal.jsx @@ -22,63 +22,70 @@ const BrowserModal = ({intl, ...props}) => ( overlayClassName={styles.modalOverlay} onRequestClose={props.onBack} > - +
+ - -

- -

-

- { /* eslint-disable max-len */ } - - { /* eslint-enable max-len */ } -

- - - + { /* eslint-enable max-len */ } +

+ + + + +
+ + + + ) + }} + /> +
-
- - - - ) - }} - /> -
-
+
); BrowserModal.propTypes = { intl: intlShape.isRequired, + isRtl: PropTypes.bool, onBack: PropTypes.func.isRequired }; -export default injectIntl(BrowserModal); +const WrappedBrowserModal = injectIntl(BrowserModal); + +WrappedBrowserModal.setAppElement = ReactModal.setAppElement; + +export default WrappedBrowserModal; diff --git a/src/components/button/button.css b/src/components/button/button.css index 9ce746c5214..9ba2fbd93b4 100644 --- a/src/components/button/button.css +++ b/src/components/button/button.css @@ -11,10 +11,17 @@ } .icon { - margin-right: .5rem; height: 1.5rem; } +[dir="ltr"] .icon { + margin-right: .5rem; +} + +[dir="rtl"] .icon { + margin-left: .5rem; +} + .content { white-space: nowrap; } diff --git a/src/components/camera-modal/camera-modal.css b/src/components/camera-modal/camera-modal.css index 4ea962fbafd..7522fbb2ec2 100644 --- a/src/components/camera-modal/camera-modal.css +++ b/src/components/camera-modal/camera-modal.css @@ -135,6 +135,10 @@ $main-button-size: 2.75rem; color: $ui-white; } +[dir="rtl"] .retake-button img { + transform: scaleX(-1); +} + @keyframes flash { 0% { opacity: 1; } 100% { opacity: 0; } diff --git a/src/components/camera-modal/camera-modal.jsx b/src/components/camera-modal/camera-modal.jsx index f115a6f8a75..8c499c36249 100644 --- a/src/components/camera-modal/camera-modal.jsx +++ b/src/components/camera-modal/camera-modal.jsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import {defineMessages, injectIntl, intlShape} from 'react-intl'; import Box from '../box/box.jsx'; -import Modal from '../modal/modal.jsx'; +import Modal from '../../containers/modal.jsx'; import styles from './camera-modal.css'; import backIcon from './icon--back.svg'; import cameraIcon from '../action-menu/icon--camera.svg'; @@ -79,7 +79,7 @@ const CameraModal = ({intl, ...props}) => ( {props.capture ? + )} + {props.phase === PHASES.pressbutton && ( +
+ + +
+ )} + {props.phase === PHASES.notfound && ( + + )} +
+
+); + +AutoScanningStep.propTypes = { + onRefresh: PropTypes.func, + onStartScan: PropTypes.func, + peripheralButtonImage: PropTypes.string, + phase: PropTypes.oneOf(Object.keys(PHASES)) +}; + +AutoScanningStep.defaultProps = { + phase: PHASES.prescan +}; + +export { + AutoScanningStep as default, + PHASES +}; diff --git a/src/components/connection-modal/connected-step.jsx b/src/components/connection-modal/connected-step.jsx index c02f644bc4e..aa71c63c8a1 100644 --- a/src/components/connection-modal/connected-step.jsx +++ b/src/components/connection-modal/connected-step.jsx @@ -12,10 +12,10 @@ const ConnectedStep = props => ( -
+
( ); ConnectedStep.propTypes = { - deviceImage: PropTypes.string.isRequired, onCancel: PropTypes.func, - onDisconnect: PropTypes.func + onDisconnect: PropTypes.func, + peripheralImage: PropTypes.string.isRequired }; export default ConnectedStep; diff --git a/src/components/connection-modal/connecting-step.jsx b/src/components/connection-modal/connecting-step.jsx index 063e67a826c..e4753628677 100644 --- a/src/components/connection-modal/connecting-step.jsx +++ b/src/components/connection-modal/connecting-step.jsx @@ -14,10 +14,10 @@ const ConnectingStep = props => ( -
+
( - + {props.connectingMessage} ( ); ConnectingStep.propTypes = { - deviceImage: PropTypes.string.isRequired, - onDisconnect: PropTypes.func + connectingMessage: PropTypes.node.isRequired, + onDisconnect: PropTypes.func, + peripheralImage: PropTypes.string.isRequired }; export default ConnectingStep; diff --git a/src/components/connection-modal/connection-modal.css b/src/components/connection-modal/connection-modal.css index b636b4af9cd..f024489c591 100644 --- a/src/components/connection-modal/connection-modal.css +++ b/src/components/connection-modal/connection-modal.css @@ -25,14 +25,14 @@ align-items: center; } -.device-tile-pane { +.peripheral-tile-pane { overflow-y: auto; width: 100%; height: 100%; padding: 0.5rem; } -.device-tile { +.peripheral-tile { display: flex; flex-direction: row; justify-content: space-between; @@ -46,32 +46,36 @@ margin-bottom: 0.5rem; } -.device-tile-name { +.peripheral-tile-name { display: flex; align-items: center; } -.device-tile-image { +[dir="ltr"] .peripheral-tile-image { margin-right: 0.5rem; } -.device-tile-name-wrapper { +[dir="rtl"] .peripheral-tile-image { + margin-left: 0.5rem; +} + +.peripheral-tile-name-wrapper { display: flex; flex-direction: column; justify-content: center; align-items: flex-start; } -.device-tile-name-label { +.peripheral-tile-name-label { font-weight: bold; font-size: 0.625rem; } -.device-tile-name-text { +.peripheral-tile-name-text { font-size: 0.875rem; } -.device-tile button { +.peripheral-tile button { padding: 0.6rem 0.75rem; border: none; border-radius: 0.25rem; @@ -89,9 +93,16 @@ align-items: flex-end; width: 22px; height: 16px; +} + +[dir="ltr"] .signal-strength-meter { margin-right: 1rem; } +[dir="rtl"] .signal-strength-meter { + margin-left: 1rem; +} + .signal-bar { width: 4px; border-radius: 4px; @@ -107,33 +118,60 @@ background-color: $pen-primary; } -.radar { +.radar-small { width: 40px; height: 40px; +} + +[dir="ltr"] .radar-small { margin-right: 0.5rem; +} + +[dir="rtl"] .radar-small { + margin-left: 0.5rem; +} + +.radar-big { + width: 120px; + height: 120px; +} + +.radar-spin { animation: spin 4s linear infinite; } +[dir="ltr"] .radar { + margin-right: .5rem; +} + +[dir="rtl"] .radar { + margin-left: .5rem; +} + @keyframes spin { 100% { transform: rotate(360deg); } } - -.device-activity { +.peripheral-activity { position: relative; } -.device-activity-icon { +.peripheral-activity-icon { /* width: 80px; height: 80px; */ } +.peripheral-button-image { + position: absolute; +} + .bluetooth-connecting-icon { position: absolute; top: -5px; right: -15px; + left: -15px; padding: 5px 5px; background-color: $motion-primary; border-radius: 100%; @@ -149,19 +187,17 @@ } } - .bluetooth-connected-icon { position: absolute; top: -5px; right: -15px; + left: -15px; padding: 5px 5px; background-color: $pen-primary; border-radius: 100%; box-shadow: 0px 0px 0px 4px $pen-transparent; } - - @keyframes wiggle { 0% {transform: rotate(3deg) scale(1.05);} 25% {transform: rotate(-3deg) scale(1.05);} @@ -170,7 +206,15 @@ 100% {transform: rotate(0deg) scale(1.05);} } -.device-tile-widgets { +.bluetooth-centered-icon { + position: absolute; + padding: 5px 5px; + background-color: $motion-primary; + border-radius: 100%; + box-shadow: 0px 0px 0px 2px $motion-transparent; +} + +.peripheral-tile-widgets { display: flex; align-items: center; } @@ -183,8 +227,59 @@ align-items: center; } +.scratch-link-help { + display: flex; + flex-direction: column; + justify-content: space-around; + height: 100%; + padding: 1rem 0; +} + +.scratch-link-help-step { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; +} + +[dir="ltr"] .scratch-link-help-step { + margin-left: 3rem; +} + +[dir="rtl"] .scratch-link-help-step { + margin-right: 3rem; +} + .scratch-link-icon { - max-width: 150px; + max-width: 50px; +} + +[dir="ltr"] .help-step-image { + margin-right: 0.5rem; +} + +[dir="rtl"] .help-step-image { + margin-left: 0.5rem; +} + +.help-step-number { + background: $pen-primary; + border-radius: 100%; + display: flex; + justify-content: center; + align-items: center; + color: $ui-white; + font-weight: bold; + min-width: 2rem; + height: 2rem; +} + +[dir="ltr"] .help-step-number { + margin-right: 0.5rem; +} + +[dir="rtl"] .help-step-number { + margin-left: 0.5rem; } .button-row { @@ -232,14 +327,26 @@ border-bottom-left-radius: 0; } -.button-icon-right { +[dir="ltr"] .button-icon-right { margin-left: 0.5rem; } +[dir="rtl"] .button-icon-right { + margin-right: 0.5rem; +} -.button-icon-left { +[dir="ltr"] .button-icon-left { margin-right: 0.5rem; } +[dir="rtl"] .button-icon-left { + margin-left: 0.5rem; +} + +/* reverse back arrow icon for RTL, don't reverse other connection icons */ +[dir="rtl"] .button-icon-back { + transform: scaleX(-1); +} + .red-button { background: $red-primary; } @@ -262,7 +369,7 @@ .instructions { text-align: center; - padding: 1rem; + margin-top: 1rem; } .dots-row { @@ -270,7 +377,7 @@ flex-direction: row; justify-content: center; align-items: center; - padding-bottom: 1rem; + margin: 1rem; } .dots-holder { diff --git a/src/components/connection-modal/connection-modal.jsx b/src/components/connection-modal/connection-modal.jsx index 9a77e92f56e..99c9a6a531f 100644 --- a/src/components/connection-modal/connection-modal.jsx +++ b/src/components/connection-modal/connection-modal.jsx @@ -3,9 +3,10 @@ import React from 'react'; import keyMirror from 'keymirror'; import Box from '../box/box.jsx'; -import Modal from '../modal/modal.jsx'; +import Modal from '../../containers/modal.jsx'; import ScanningStep from '../../containers/scanning-step.jsx'; +import AutoScanningStep from '../../containers/auto-scanning-step.jsx'; import ConnectingStep from './connecting-step.jsx'; import ConnectedStep from './connected-step.jsx'; import ErrorStep from './error-step.jsx'; @@ -26,11 +27,13 @@ const ConnectionModalComponent = props => ( className={styles.modalContent} contentLabel={props.name} headerClassName={styles.header} - headerImage={props.smallDeviceImage} + headerImage={props.smallPeripheralImage} + onHelp={props.onHelp} onRequestClose={props.onCancel} > - {props.phase === PHASES.scanning && } + {props.phase === PHASES.scanning && !props.useAutoScan && } + {props.phase === PHASES.scanning && props.useAutoScan && } {props.phase === PHASES.connecting && } {props.phase === PHASES.connected && } {props.phase === PHASES.error && } @@ -40,11 +43,15 @@ const ConnectionModalComponent = props => ( ); ConnectionModalComponent.propTypes = { + connectingMessage: PropTypes.node, name: PropTypes.node, onCancel: PropTypes.func.isRequired, + onHelp: PropTypes.func.isRequired, + peripheralButtonImage: PropTypes.string, phase: PropTypes.oneOf(Object.keys(PHASES)).isRequired, - smallDeviceImage: PropTypes.string, - title: PropTypes.string.isRequired + smallPeripheralImage: PropTypes.string, + title: PropTypes.string.isRequired, + useAutoScan: PropTypes.bool.isRequired }; export { diff --git a/src/components/connection-modal/error-step.jsx b/src/components/connection-modal/error-step.jsx index 681d74b72a2..21dcd8d54c9 100644 --- a/src/components/connection-modal/error-step.jsx +++ b/src/components/connection-modal/error-step.jsx @@ -1,5 +1,6 @@ import {FormattedMessage} from 'react-intl'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import React from 'react'; import Box from '../box/box.jsx'; @@ -13,10 +14,10 @@ const ErrorStep = props => ( -
+
@@ -26,7 +27,7 @@ const ErrorStep = props => (
( onClick={props.onScanning} >
@@ -68,9 +69,9 @@ const ErrorStep = props => ( ); ErrorStep.propTypes = { - deviceImage: PropTypes.string.isRequired, onHelp: PropTypes.func, - onScanning: PropTypes.func + onScanning: PropTypes.func, + peripheralImage: PropTypes.string.isRequired }; export default ErrorStep; diff --git a/src/components/connection-modal/icons/bluetooth.svg b/src/components/connection-modal/icons/bluetooth.svg new file mode 100644 index 00000000000..ebec155b439 --- /dev/null +++ b/src/components/connection-modal/icons/bluetooth.svg @@ -0,0 +1,31 @@ + + + + Bluetooth + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/connection-modal/icons/scratch-link.png b/src/components/connection-modal/icons/scratch-link.png deleted file mode 100644 index af7ceed4baa..00000000000 Binary files a/src/components/connection-modal/icons/scratch-link.png and /dev/null differ diff --git a/src/components/connection-modal/icons/scratchlink.svg b/src/components/connection-modal/icons/scratchlink.svg new file mode 100644 index 00000000000..de2011a5efd --- /dev/null +++ b/src/components/connection-modal/icons/scratchlink.svg @@ -0,0 +1,34 @@ + + + + scratchlink + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/connection-modal/icons/searching.png b/src/components/connection-modal/icons/searching.png index 260f3227f2e..7a72005a102 100644 Binary files a/src/components/connection-modal/icons/searching.png and b/src/components/connection-modal/icons/searching.png differ diff --git a/src/components/connection-modal/device-tile.jsx b/src/components/connection-modal/peripheral-tile.jsx similarity index 77% rename from src/components/connection-modal/device-tile.jsx rename to src/components/connection-modal/peripheral-tile.jsx index c806453aa0f..6b7e0388d62 100644 --- a/src/components/connection-modal/device-tile.jsx +++ b/src/components/connection-modal/peripheral-tile.jsx @@ -7,7 +7,7 @@ import Box from '../box/box.jsx'; import styles from './connection-modal.css'; -class DeviceTile extends React.Component { +class PeripheralTile extends React.Component { constructor (props) { super(props); bindAll(this, [ @@ -19,26 +19,26 @@ class DeviceTile extends React.Component { } render () { return ( - - + + - - + + - + {this.props.name} - +
( {props.scanning ? ( - props.deviceList.length === 0 ? ( + props.peripheralList.length === 0 ? (
) : ( - - {props.deviceList.map(device => - ( + {props.peripheralList.map(peripheral => + () )} - +
) ) : ( )} @@ -84,19 +85,19 @@ const ScanningStep = props => ( ); ScanningStep.propTypes = { - deviceList: PropTypes.arrayOf(PropTypes.shape({ + onConnecting: PropTypes.func, + onRefresh: PropTypes.func, + peripheralList: PropTypes.arrayOf(PropTypes.shape({ name: PropTypes.string, rssi: PropTypes.number, peripheralId: PropTypes.string })), - onConnecting: PropTypes.func, - onRefresh: PropTypes.func, scanning: PropTypes.bool.isRequired, - smallDeviceImage: PropTypes.string + smallPeripheralImage: PropTypes.string }; ScanningStep.defaultProps = { - deviceList: [], + peripheralList: [], scanning: true }; diff --git a/src/components/connection-modal/unavailable-step.jsx b/src/components/connection-modal/unavailable-step.jsx index 2f789cbdb0e..ae89404a195 100644 --- a/src/components/connection-modal/unavailable-step.jsx +++ b/src/components/connection-modal/unavailable-step.jsx @@ -1,35 +1,60 @@ import {FormattedMessage} from 'react-intl'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import React from 'react'; import Box from '../box/box.jsx'; import Dots from './dots.jsx'; import helpIcon from './icons/help.svg'; import backIcon from './icons/back.svg'; -import scratchLinkIcon from './icons/scratch-link.png'; +import bluetoothIcon from './icons/bluetooth.svg'; +import scratchLinkIcon from './icons/scratchlink.svg'; import styles from './connection-modal.css'; const UnavailableStep = props => ( - -
- +
+
+
+ {'1'} +
+
+ +
+
+ +
- +
+
+ {'2'} +
+
+ +
+
+ +
+
+
-
- -
( onClick={props.onScanning} >
diff --git a/src/components/costume-canvas/costume-canvas.jsx b/src/components/costume-canvas/costume-canvas.jsx deleted file mode 100644 index 4375572c1e2..00000000000 --- a/src/components/costume-canvas/costume-canvas.jsx +++ /dev/null @@ -1,134 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import svgToImage from 'svg-to-image'; -import xhr from 'xhr'; - -/** - * @fileoverview - * A component for rendering Scratch costume URLs to canvases. - * Use for sprite library, costume library, sprite selector, etc. - * Props include width, height, and direction (direction in Scratch value). - */ - -class CostumeCanvas extends React.Component { - componentDidMount () { - this.load(); - } - componentDidUpdate (prevProps) { - if (prevProps.url !== this.props.url) { - this.load(); - } else if ( - prevProps.width !== this.props.width || - prevProps.height !== this.props.height || - prevProps.direction !== this.props.direction - ) { - this.draw(); - } - } - draw () { - if (!this.canvas) { - return; - } - - // Draw the costume to the rendered canvas. - const img = this.img; - const context = this.canvas.getContext('2d'); - - // Scale to fit. - let scale; - - // Choose the larger dimension to scale by. - if (img.width > img.height) { - scale = this.canvas.width / img.width; - } else { - scale = this.canvas.height / img.height; - } - - // Rotate by the Scratch-value direction. - const angle = (-90 + this.props.direction) * Math.PI / 180; - - // Rotation origin point will be center of the canvas. - const contextTranslateX = this.canvas.width / 2; - const contextTranslateY = this.canvas.height / 2; - - // First, clear the canvas. - context.clearRect(0, 0, - this.canvas.width, this.canvas.height); - - // Translate the context to the center of the canvas, - // then rotate canvas drawing by `angle`. - context.translate(contextTranslateX, contextTranslateY); - context.rotate(angle); - context.drawImage(img, - 0, 0, img.width, img.height, - -(scale * img.width / 2), -(scale * img.height / 2), - scale * img.width, - scale * img.height); - - // Reset the canvas rotation and translation to 0, (0, 0). - context.rotate(-angle); - context.translate(-contextTranslateX, -contextTranslateY); - } - load () { - // Draw the icon on our canvas. - const url = this.props.url; - if (url.indexOf('.svg') > -1) { - // Vector graphics: need to download with XDR and rasterize. - // Queue request asynchronously. - setTimeout(() => { - xhr.get({ - useXDR: true, - url: url - }, (err, response, body) => { - if (!err) { - svgToImage(body, (svgErr, img) => { - if (!svgErr) { - this.img = img; - this.draw(); - } - }); - } - }); - }, 0); - - } else { - // Raster graphics: create Image and draw it. - const img = new Image(); - img.src = url; - img.onload = () => { - this.img = img; - this.draw(); - }; - } - } - render () { - return ( - (this.canvas = c)} // eslint-disable-line react/jsx-sort-props - /> - ); - } -} - -CostumeCanvas.defaultProps = { - width: 100, - height: 100, - direction: 90 -}; - -CostumeCanvas.propTypes = { - className: PropTypes.string, - direction: PropTypes.number, - height: PropTypes.number, - url: PropTypes.string.isRequired, - width: PropTypes.number -}; - -export default CostumeCanvas; diff --git a/src/components/crash-message/crash-message.jsx b/src/components/crash-message/crash-message.jsx index 8ea44473986..764c8e2167d 100644 --- a/src/components/crash-message/crash-message.jsx +++ b/src/components/crash-message/crash-message.jsx @@ -1,12 +1,7 @@ -/* eslint-disable react/jsx-no-literals */ -/* - @todo Rule is disabled because this component is rendered outside the - intl provider right now so cannot be translated. -*/ - import PropTypes from 'prop-types'; import React from 'react'; import Box from '../box/box.jsx'; +import {FormattedMessage} from 'react-intl'; import styles from './crash-message.css'; import reloadIcon from './reload.svg'; @@ -19,19 +14,30 @@ const CrashMessage = props => ( src={reloadIcon} />

- Oops! Something went wrong. +

- We are so sorry, but it looks like Scratch has crashed. This bug has been - automatically reported to the Scratch Team. Please refresh your page to try - again. - +

diff --git a/src/components/custom-procedures/custom-procedures.css b/src/components/custom-procedures/custom-procedures.css index 6c8967a12d5..1716baab306 100644 --- a/src/components/custom-procedures/custom-procedures.css +++ b/src/components/custom-procedures/custom-procedures.css @@ -91,6 +91,10 @@ color: white; } -.button-row button + button { +[dir="ltr"] .button-row button + button { margin-left: 0.5rem; } + +[dir="rtl"] .button-row button + button { + margin-right: 0.5rem; +} diff --git a/src/components/custom-procedures/custom-procedures.jsx b/src/components/custom-procedures/custom-procedures.jsx index 08131ea5b1d..7d2f3e1a535 100644 --- a/src/components/custom-procedures/custom-procedures.jsx +++ b/src/components/custom-procedures/custom-procedures.jsx @@ -1,8 +1,8 @@ import PropTypes from 'prop-types'; import React from 'react'; -import Modal from '../modal/modal.jsx'; +import Modal from '../../containers/modal.jsx'; import Box from '../box/box.jsx'; -import {FormattedMessage} from 'react-intl'; +import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl'; import booleanInputIcon from './icon--boolean-input.svg'; import textInputIcon from './icon--text-input.svg'; @@ -10,10 +10,18 @@ import labelIcon from './icon--label.svg'; import styles from './custom-procedures.css'; +const messages = defineMessages({ + myblockModalTitle: { + defaultMessage: 'Make a Block', + description: 'Title for the modal where you create a custom block.', + id: 'gui.customProcedures.myblockModalTitle' + } +}); + const CustomProcedures = props => ( ( CustomProcedures.propTypes = { componentRef: PropTypes.func.isRequired, + intl: intlShape, onAddBoolean: PropTypes.func.isRequired, onAddLabel: PropTypes.func.isRequired, onAddTextNumber: PropTypes.func.isRequired, @@ -142,4 +151,4 @@ CustomProcedures.propTypes = { warp: PropTypes.bool.isRequired }; -export default CustomProcedures; +export default injectIntl(CustomProcedures); diff --git a/src/components/custom-procedures/icon--label.svg b/src/components/custom-procedures/icon--label.svg index 2c2d52a66da..c46c2f00cc8 100644 --- a/src/components/custom-procedures/icon--label.svg +++ b/src/components/custom-procedures/icon--label.svg @@ -1 +1 @@ -R1_ C.Procedure Editble Inputstext \ No newline at end of file +R1_ C.Procedure Editble Inputstext diff --git a/src/components/direction-picker/dial.css b/src/components/direction-picker/dial.css new file mode 100644 index 00000000000..ce9b7daea87 --- /dev/null +++ b/src/components/direction-picker/dial.css @@ -0,0 +1,41 @@ +@import "../../css/colors.css"; + +.container { + padding: 1rem; + display: flex; + justify-content: center; + align-items: center; + user-select: none; +} + +.dial-container { + position: relative; +} + +.dial-face, .dial-handle, .gauge { + position: absolute; + top: 0; + left: 0; + overflow: visible; +} + +.dial-face { + width: 100%; +} + +$dial-size: 40px; + +.dial-handle { + cursor: pointer; + width: $dial-size; + height: $dial-size; + /* Use margin to make positioning via top/left easier */ + margin-left: calc($dial-size / -2); + margin-top: calc($dial-size / -2); +} + +.gauge-path { + fill: $motion-transparent; + stroke: $motion-primary; + stroke-width: 1px; +} diff --git a/src/components/direction-picker/dial.jsx b/src/components/direction-picker/dial.jsx new file mode 100644 index 00000000000..df97fda7fa4 --- /dev/null +++ b/src/components/direction-picker/dial.jsx @@ -0,0 +1,156 @@ +import PropTypes from 'prop-types'; +import bindAll from 'lodash.bindall'; +import React from 'react'; +import {getEventXY} from '../../lib/touch-utils'; + +import styles from './dial.css'; + +import dialFace from './icon--dial.svg'; +import dialHandle from './icon--handle.svg'; + +class Dial extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleMouseDown', + 'handleMouseMove', + 'containerRef', + 'handleRef', + 'unbindMouseEvents' + ]); + } + + componentDidMount () { + // Manually add touch/mouse handlers so that preventDefault can be used + // to prevent scrolling on touch. + // Tracked as a react issue https://github.com/facebook/react/issues/6436 + this.handleElement.addEventListener('mousedown', this.handleMouseDown); + this.handleElement.addEventListener('touchstart', this.handleMouseDown); + } + + componentWillUnmount () { + this.unbindMouseEvents(); + this.handleElement.removeEventListener('mousedown', this.handleMouseDown); + this.handleElement.removeEventListener('touchstart', this.handleMouseDown); + } + + /** + * Get direction from dial center to mouse move event. + * @param {Event} e - Mouse move event. + * @returns {number} Direction in degrees, clockwise, 90=horizontal. + */ + directionToMouseEvent (e) { + const {x: mx, y: my} = getEventXY(e); + const bbox = this.containerElement.getBoundingClientRect(); + const cy = bbox.top + (bbox.height / 2); + const cx = bbox.left + (bbox.width / 2); + const angle = Math.atan2(my - cy, mx - cx); + const degrees = angle * (180 / Math.PI); + return degrees + 90; // To correspond with scratch coordinate system + } + + /** + * Create SVG path data string for the dial "gauge", the overlaid arc slice. + * @param {number} radius - The radius of the dial. + * @param {number} direction - Direction in degrees, clockwise, 90=horizontal. + * @returns {string} Path data string for the gauge. + */ + gaugePath (radius, direction) { + const rads = (direction) * (Math.PI / 180); + const path = []; + path.push(`M ${radius} 0`); + path.push(`L ${radius} ${radius}`); + path.push(`L ${radius + (radius * Math.sin(rads))} ${radius - (radius * Math.cos(rads))}`); + path.push(`A ${radius} ${radius} 0 0 ${direction < 0 ? 1 : 0} ${radius} 0`); + path.push(`Z`); + return path.join(' '); + } + + handleMouseMove (e) { + this.props.onChange(this.directionToMouseEvent(e) + this.directionOffset); + e.preventDefault(); + } + + unbindMouseEvents () { + window.removeEventListener('mousemove', this.handleMouseMove); + window.removeEventListener('mouseup', this.unbindMouseEvents); + window.removeEventListener('touchmove', this.handleMouseMove); + window.removeEventListener('touchend', this.unbindMouseEvents); + } + + handleMouseDown (e) { + // Because the drag handle is not a single point, there is some initial + // difference between the current sprite direction and the direction to the mouse + // Store this offset to prevent jumping when the mouse is moved. + this.directionOffset = this.props.direction - this.directionToMouseEvent(e); + window.addEventListener('mousemove', this.handleMouseMove); + window.addEventListener('mouseup', this.unbindMouseEvents); + window.addEventListener('touchmove', this.handleMouseMove); + window.addEventListener('touchend', this.unbindMouseEvents); + e.preventDefault(); + } + + containerRef (el) { + this.containerElement = el; + } + + handleRef (el) { + this.handleElement = el; + } + + render () { + const {direction, radius} = this.props; + return ( +
+
+ + + + + +
+
+ ); + } +} + +Dial.propTypes = { + direction: PropTypes.number, + onChange: PropTypes.func.isRequired, + radius: PropTypes.number +}; + +Dial.defaultProps = { + direction: 90, // degrees + radius: 56 // px +}; + +export default Dial; diff --git a/src/components/direction-picker/direction-picker.css b/src/components/direction-picker/direction-picker.css new file mode 100644 index 00000000000..501ff021714 --- /dev/null +++ b/src/components/direction-picker/direction-picker.css @@ -0,0 +1,32 @@ +@import "../../css/colors.css"; + +.button-row { + display: flex; + flex-direction: row; + justify-content: center; + +} + +.icon-button { + margin: 0.25rem; + border: none; + background: none; + outline: none; + cursor: pointer; + user-select: none; +} + +.icon-button:active > img { + width: 20px; + height: 20px; + transform: scale(1.15); +} + +.icon-button > img { + transition: transform 0.1s; + filter: grayscale(100%); +} + +.icon-button.active > img { + filter: none; +} diff --git a/src/components/direction-picker/direction-picker.jsx b/src/components/direction-picker/direction-picker.jsx new file mode 100644 index 00000000000..84a15db7a7e --- /dev/null +++ b/src/components/direction-picker/direction-picker.jsx @@ -0,0 +1,148 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import Popover from 'react-popover'; +import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; + +import Label from '../forms/label.jsx'; +import Input from '../forms/input.jsx'; +import BufferedInputHOC from '../forms/buffered-input-hoc.jsx'; +import Dial from './dial.jsx'; + +import styles from './direction-picker.css'; + +import allAroundIcon from './icon--all-around.svg'; +import leftRightIcon from './icon--left-right.svg'; +import dontRotateIcon from './icon--dont-rotate.svg'; + +const BufferedInput = BufferedInputHOC(Input); + +const directionLabel = ( + +); + +const RotationStyles = { + ALL_AROUND: 'all around', + LEFT_RIGHT: 'left-right', + DONT_ROTATE: "don't rotate" +}; + +const messages = defineMessages({ + allAround: { + id: 'gui.directionPicker.rotationStyles.allAround', + description: 'Button to change to the all around rotation style', + defaultMessage: 'All Around' + }, + leftRight: { + id: 'gui.directionPicker.rotationStyles.leftRight', + description: 'Button to change to the left-right rotation style', + defaultMessage: 'Left/Right' + }, + dontRotate: { + id: 'gui.directionPicker.rotationStyles.dontRotate', + description: 'Button to change to the dont rotate rotation style', + defaultMessage: 'Do not rotate' + } +}); + +const DirectionPicker = props => ( +
+ } + isOpen={props.popoverOpen} + preferPlace="above" + onOuterAction={props.onClosePopover} + > + + + + +); + +DirectionPicker.propTypes = { + direction: PropTypes.number, + disabled: PropTypes.bool.isRequired, + intl: intlShape, + labelAbove: PropTypes.bool, + onChangeDirection: PropTypes.func.isRequired, + onClickAllAround: PropTypes.func.isRequired, + onClickDontRotate: PropTypes.func.isRequired, + onClickLeftRight: PropTypes.func.isRequired, + onClosePopover: PropTypes.func.isRequired, + onOpenPopover: PropTypes.func.isRequired, + popoverOpen: PropTypes.bool.isRequired, + rotationStyle: PropTypes.string +}; + +DirectionPicker.defaultProps = { + labelAbove: false +}; + +const WrappedDirectionPicker = injectIntl(DirectionPicker); + +export { + WrappedDirectionPicker as default, + RotationStyles +}; diff --git a/src/components/direction-picker/icon--all-around.svg b/src/components/direction-picker/icon--all-around.svg new file mode 100644 index 00000000000..2412c0b2327 --- /dev/null +++ b/src/components/direction-picker/icon--all-around.svg @@ -0,0 +1,10 @@ + + + + all-around-active + Created with Sketch. + + + + + \ No newline at end of file diff --git a/src/components/direction-picker/icon--dial.svg b/src/components/direction-picker/icon--dial.svg new file mode 100644 index 00000000000..d4aa8ed8205 --- /dev/null +++ b/src/components/direction-picker/icon--dial.svg @@ -0,0 +1 @@ +dial-face \ No newline at end of file diff --git a/src/components/direction-picker/icon--dont-rotate.svg b/src/components/direction-picker/icon--dont-rotate.svg new file mode 100644 index 00000000000..4796c03af25 --- /dev/null +++ b/src/components/direction-picker/icon--dont-rotate.svg @@ -0,0 +1,10 @@ + + + + dont-rotate-active + Created with Sketch. + + + + + \ No newline at end of file diff --git a/src/components/direction-picker/icon--handle.svg b/src/components/direction-picker/icon--handle.svg new file mode 100644 index 00000000000..8e5fee6e0ba --- /dev/null +++ b/src/components/direction-picker/icon--handle.svg @@ -0,0 +1 @@ +01_Dial Elements \ No newline at end of file diff --git a/src/components/direction-picker/icon--left-right.svg b/src/components/direction-picker/icon--left-right.svg new file mode 100644 index 00000000000..4525bd6e917 --- /dev/null +++ b/src/components/direction-picker/icon--left-right.svg @@ -0,0 +1,14 @@ + + + + left-right-active + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/src/components/filter/filter.css b/src/components/filter/filter.css index b67e60795c2..fffef4812ec 100644 --- a/src/components/filter/filter.css +++ b/src/components/filter/filter.css @@ -18,13 +18,22 @@ .filter-icon { position: absolute; top: 0; - left: 0; height: 1rem; width: 1rem; +} + +[dir="ltr"] .filter-icon { + left: 0; margin: 0.75rem 0.75rem 0.75rem 1rem; } +[dir="rtl"] .filter-icon { + right: 0; + margin: 0.75rem 1rem 0.75rem 0.75rem; + transform: scaleX(-1); +} + .filter:focus-within { box-shadow: 0 0 0 .25rem $motion-transparent; } @@ -36,7 +45,6 @@ opacity: 0; position: absolute; top: 0; - right: 0; display: flex; justify-content: center; @@ -53,6 +61,14 @@ transition: opacity 0.05s linear; } +[dir="ltr"] .x-icon-wrapper { + right: 0; +} + +[dir="rtl"] .x-icon-wrapper { + left: 0; +} + /* Shown state */ @@ -96,13 +112,27 @@ font-size: 0.75rem; letter-spacing: 0.15px; cursor: text; +} + +[dir="ltr"] .filter-input { padding: .625rem 2rem .625rem 3rem; } +[dir="rtl"] .filter-input { + padding: .625rem 3rem .625rem 2rem; +} + .filter-input::placeholder { opacity: .5; - padding: 0 0 0 0.25rem; color: $text-primary; font-size: 0.875rem; letter-spacing: 0.15px; } + +[dir="ltr"] .filter-input::placeholder { + padding: 0 0 0 0.25rem; +} + +[dir="rtl"] .filter-input::placeholder { + padding: 0 0.25rem 0 0; +} diff --git a/src/components/forms/label.css b/src/components/forms/label.css index 759a731d37e..ec27274c7f7 100644 --- a/src/components/forms/label.css +++ b/src/components/forms/label.css @@ -7,15 +7,32 @@ align-items: center; } +.input-group-column { + display: inline-flex; + flex-direction: column; + align-items: flex-start; +} + +.input-group-column span { + margin-bottom: .25rem; +} + .input-label, .input-label-secondary { font-size: 0.625rem; - margin-right: .5rem; user-select: none; cursor: default; white-space: nowrap; } +[dir="ltr"] .input-label, [dir="ltr"] .input-label-secondary { + margin-right: .5rem; +} + +[dir="rtl"] .input-label, [dir="rtl"] .input-label-secondary { + margin-left: .5rem; +} + .input-label { font-weight: bold; } diff --git a/src/components/forms/label.jsx b/src/components/forms/label.jsx index e005844587e..8b90ad1e899 100644 --- a/src/components/forms/label.jsx +++ b/src/components/forms/label.jsx @@ -4,7 +4,7 @@ import React from 'react'; import styles from './label.css'; const Label = props => ( -