From f3d1a91533a17241a064312da08f15c944c7511c Mon Sep 17 00:00:00 2001 From: Chris Garrity Date: Tue, 18 Jun 2019 10:16:55 -0400 Subject: [PATCH 1/8] Add play buttons to sound library tiles for touch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add prop to library items to show a play button. If a play button is present the ‘play on hover’ only applies over the icon rather than the full tile. --- src/components/library-item/icon--play.svg | 27 ++++++++++++ src/components/library-item/library-item.css | 43 ++++++++++++++++++++ src/components/library-item/library-item.jsx | 38 ++++++++++++++--- src/components/library/library.jsx | 5 ++- src/containers/library-item.jsx | 10 ++++- src/containers/sound-library.jsx | 1 + 6 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 src/components/library-item/icon--play.svg diff --git a/src/components/library-item/icon--play.svg b/src/components/library-item/icon--play.svg new file mode 100644 index 000000000..7c4f45df7 --- /dev/null +++ b/src/components/library-item/icon--play.svg @@ -0,0 +1,27 @@ + + + + play + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/library-item/library-item.css b/src/components/library-item/library-item.css index 5e726a363..128c9f93e 100644 --- a/src/components/library-item/library-item.css +++ b/src/components/library-item/library-item.css @@ -7,6 +7,7 @@ align-items: center; justify-content: flex-start; flex-basis: 160px; + position: relative; height: 160px; max-width: 160px; margin: $space; @@ -200,3 +201,45 @@ [dir="rtl"] .coming-soon-text { transform: translate(calc(-2 * $space), calc(2 * $space)); } + +.play-button { + display: flex; + align-items: center; + justify-content: center; + + overflow: hidden; /* Mask the icon animation */ + width: 1.5rem; + height: 1.5rem; + background-color: $motion-primary; + color: $ui-white; + border-radius: 50%; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + user-select: none; + cursor: pointer; + transition: all 0.15s ease-out; +} + +.play-button { + position: absolute; + position: absolute; + top: 0.125rem; + z-index: auto; +} + +.play-button:focus { + outline: none; +} + +.play-icon { + width: 50%; +} + +[dir="ltr"] .play-button { + right: 0.125rem; + padding-left: .125rem; +} + +[dir="rtl"] .play-button { + left: 0.125rem; + padding-right: .125rem; +} diff --git a/src/components/library-item/library-item.jsx b/src/components/library-item/library-item.jsx index 5af6fb159..3101e4279 100644 --- a/src/components/library-item/library-item.jsx +++ b/src/components/library-item/library-item.jsx @@ -8,6 +8,11 @@ import classNames from 'classnames'; import bluetoothIconURL from './bluetooth.svg'; import internetConnectionIconURL from './internet-connection.svg'; +import playIcon from './icon--play.svg'; + +const preventClick = e => { + e.stopPropagation(); +}; /* eslint-disable react/prefer-stateless-function */ class LibraryItemComponent extends React.PureComponent { @@ -115,12 +120,16 @@ class LibraryItemComponent extends React.PureComponent { onClick={this.props.onClick} onFocus={this.props.onFocus} onKeyPress={this.props.onKeyPress} - onMouseEnter={this.props.onMouseEnter} - onMouseLeave={this.props.onMouseLeave} + onMouseEnter={this.props.showPlayButton ? null : this.props.onMouseEnter} + onMouseLeave={this.props.showPlayButton ? null : this.props.onMouseLeave} > {/* Layers of wrapping is to prevent layout thrashing on animation */} - + {this.props.name} + {this.props.showPlayButton ? ( +
+ +
+ ) : null}
); } @@ -158,11 +183,14 @@ LibraryItemComponent.propTypes = { onFocus: PropTypes.func.isRequired, onKeyPress: PropTypes.func.isRequired, onMouseEnter: PropTypes.func.isRequired, - onMouseLeave: PropTypes.func.isRequired + onMouseLeave: PropTypes.func.isRequired, + onPlay: PropTypes.func.isRequired, + showPlayButton: PropTypes.bool }; LibraryItemComponent.defaultProps = { - disabled: false + disabled: false, + showPlayButton: false }; export default LibraryItemComponent; diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx index 13736d60f..3be73b34c 100644 --- a/src/components/library/library.jsx +++ b/src/components/library/library.jsx @@ -187,6 +187,7 @@ class LibraryComponent extends React.Component { internetConnectionRequired={dataItem.internetConnectionRequired} key={typeof dataItem.name === 'string' ? dataItem.name : dataItem.rawURL} name={dataItem.name} + showPlayButton={this.props.showPlayButton} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onSelect={this.handleSelect} @@ -227,12 +228,14 @@ LibraryComponent.propTypes = { onItemMouseLeave: PropTypes.func, onItemSelected: PropTypes.func, onRequestClose: PropTypes.func, + showPlayButton: PropTypes.bool, tags: PropTypes.arrayOf(PropTypes.shape(TagButton.propTypes)), title: PropTypes.string.isRequired }; LibraryComponent.defaultProps = { - filterable: true + filterable: true, + showPlayButton: false }; export default injectIntl(LibraryComponent); diff --git a/src/containers/library-item.jsx b/src/containers/library-item.jsx index 6177b916f..3ffa81485 100644 --- a/src/containers/library-item.jsx +++ b/src/containers/library-item.jsx @@ -15,6 +15,7 @@ class LibraryItem extends React.PureComponent { 'handleKeyPress', 'handleMouseEnter', 'handleMouseLeave', + 'handlePlay', 'rotateIcon', 'startRotatingIcons', 'stopRotatingIcons' @@ -62,6 +63,10 @@ class LibraryItem extends React.PureComponent { }, this.stopRotatingIcons); } } + handlePlay (e) { + e.stopPropagation(); // To prevent from bubbling back to handleClick + this.props.onMouseEnter(this.props.id); + } startRotatingIcons () { this.rotateIcon(); this.intervalId = setInterval(this.rotateIcon, 300); @@ -105,12 +110,14 @@ class LibraryItem extends React.PureComponent { insetIconURL={this.props.insetIconURL} internetConnectionRequired={this.props.internetConnectionRequired} name={this.props.name} + showPlayButton={this.props.showPlayButton} onBlur={this.handleBlur} onClick={this.handleClick} onFocus={this.handleFocus} onKeyPress={this.handleKeyPress} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} + onPlay={this.handlePlay} /> ); } @@ -143,7 +150,8 @@ LibraryItem.propTypes = { ]), onMouseEnter: PropTypes.func.isRequired, onMouseLeave: PropTypes.func.isRequired, - onSelect: PropTypes.func.isRequired + onSelect: PropTypes.func.isRequired, + showPlayButton: PropTypes.bool }; export default injectIntl(LibraryItem); diff --git a/src/containers/sound-library.jsx b/src/containers/sound-library.jsx index e49f1057a..677d93dfb 100644 --- a/src/containers/sound-library.jsx +++ b/src/containers/sound-library.jsx @@ -141,6 +141,7 @@ class SoundLibrary extends React.PureComponent { return ( Date: Tue, 25 Jun 2019 16:58:49 -0400 Subject: [PATCH 2/8] revise styles Checked with Carl and updated the style of the play button. --- src/components/library-item/library-item.css | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/library-item/library-item.css b/src/components/library-item/library-item.css index 128c9f93e..7b7079441 100644 --- a/src/components/library-item/library-item.css +++ b/src/components/library-item/library-item.css @@ -210,7 +210,7 @@ overflow: hidden; /* Mask the icon animation */ width: 1.5rem; height: 1.5rem; - background-color: $motion-primary; + background-color: $sound-primary; color: $ui-white; border-radius: 50%; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; @@ -221,8 +221,7 @@ .play-button { position: absolute; - position: absolute; - top: 0.125rem; + top: .5rem; z-index: auto; } @@ -235,11 +234,9 @@ } [dir="ltr"] .play-button { - right: 0.125rem; - padding-left: .125rem; + left: .5rem; } [dir="rtl"] .play-button { - left: 0.125rem; - padding-right: .125rem; + right: .5rem; } From 6216e7dfa7abe9152ab003a378cb44e12e799415 Mon Sep 17 00:00:00 2001 From: Chris Garrity Date: Mon, 1 Jul 2019 10:20:37 -0400 Subject: [PATCH 3/8] improve touch interactions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Play button is larger (easier to touch). Can both start and stop sounds. Sound doesn’t restart on mouseEnter if the sound is already playing. Sound stops if you move off play button Missing: stop button does not revert to play button when the sound finishes. --- src/components/library-item/icon--stop.svg | 28 ++++++++++++++++++++ src/components/library-item/library-item.css | 4 +-- src/components/library-item/library-item.jsx | 11 +++++--- src/components/library/library.jsx | 17 +++++++++--- src/containers/library-item.jsx | 10 +++++++ 5 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 src/components/library-item/icon--stop.svg diff --git a/src/components/library-item/icon--stop.svg b/src/components/library-item/icon--stop.svg new file mode 100644 index 000000000..53837e30c --- /dev/null +++ b/src/components/library-item/icon--stop.svg @@ -0,0 +1,28 @@ + + + + stop-playback + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/library-item/library-item.css b/src/components/library-item/library-item.css index 7b7079441..760153eae 100644 --- a/src/components/library-item/library-item.css +++ b/src/components/library-item/library-item.css @@ -208,8 +208,8 @@ justify-content: center; overflow: hidden; /* Mask the icon animation */ - width: 1.5rem; - height: 1.5rem; + width: 2.5rem; + height: 2.5rem; background-color: $sound-primary; color: $ui-white; border-radius: 50%; diff --git a/src/components/library-item/library-item.jsx b/src/components/library-item/library-item.jsx index 3101e4279..f63f92d39 100644 --- a/src/components/library-item/library-item.jsx +++ b/src/components/library-item/library-item.jsx @@ -9,9 +9,11 @@ import classNames from 'classnames'; import bluetoothIconURL from './bluetooth.svg'; import internetConnectionIconURL from './internet-connection.svg'; import playIcon from './icon--play.svg'; +import stopIcon from './icon--stop.svg'; const preventClick = e => { e.stopPropagation(); + e.preventDefault(); }; /* eslint-disable react/prefer-stateless-function */ @@ -141,15 +143,14 @@ class LibraryItemComponent extends React.PureComponent {
) : null} @@ -174,6 +175,7 @@ LibraryItemComponent.propTypes = { iconURL: PropTypes.string, insetIconURL: PropTypes.string, internetConnectionRequired: PropTypes.bool, + isPlaying: PropTypes.bool, name: PropTypes.oneOfType([ PropTypes.string, PropTypes.node @@ -185,6 +187,7 @@ LibraryItemComponent.propTypes = { onMouseEnter: PropTypes.func.isRequired, onMouseLeave: PropTypes.func.isRequired, onPlay: PropTypes.func.isRequired, + onStop: PropTypes.func.isRequired, showPlayButton: PropTypes.bool }; diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx index 3be73b34c..0a86c26db 100644 --- a/src/components/library/library.jsx +++ b/src/components/library/library.jsx @@ -43,7 +43,7 @@ class LibraryComponent extends React.Component { 'setFilteredDataRef' ]); this.state = { - selectedItem: null, + playingItem: null, filterQuery: '', selectedTag: ALL_TAG.tag, loaded: false @@ -75,10 +75,20 @@ class LibraryComponent extends React.Component { }); } handleMouseEnter (id) { - if (this.props.onItemMouseEnter) this.props.onItemMouseEnter(this.getFilteredData()[id]); + console.log('Library MouseEnter id:', id, ', playingItem: ', this.state.playingItem); + // don't restart if mouse over already playing item + if (this.props.onItemMouseEnter && this.state.playingItem !== id) { + this.setState({ + playingItem: id + }, this.props.onItemMouseEnter(this.getFilteredData()[id])); + } } handleMouseLeave (id) { - if (this.props.onItemMouseLeave) this.props.onItemMouseLeave(this.getFilteredData()[id]); + if (this.props.onItemMouseLeave) { + this.setState({ + playingItem: null + }, this.props.onItemMouseLeave(this.getFilteredData()[id])); + } } handleFilterChange (event) { this.setState({ @@ -185,6 +195,7 @@ class LibraryComponent extends React.Component { id={index} insetIconURL={dataItem.insetIconURL} internetConnectionRequired={dataItem.internetConnectionRequired} + isPlaying={this.state.playingItem === index} key={typeof dataItem.name === 'string' ? dataItem.name : dataItem.rawURL} name={dataItem.name} showPlayButton={this.props.showPlayButton} diff --git a/src/containers/library-item.jsx b/src/containers/library-item.jsx index 3ffa81485..75f6b2998 100644 --- a/src/containers/library-item.jsx +++ b/src/containers/library-item.jsx @@ -16,6 +16,7 @@ class LibraryItem extends React.PureComponent { 'handleMouseEnter', 'handleMouseLeave', 'handlePlay', + 'handleStop', 'rotateIcon', 'startRotatingIcons', 'stopRotatingIcons' @@ -65,8 +66,14 @@ class LibraryItem extends React.PureComponent { } handlePlay (e) { e.stopPropagation(); // To prevent from bubbling back to handleClick + e.preventDefault(); this.props.onMouseEnter(this.props.id); } + handleStop (e) { + e.stopPropagation(); // To prevent from bubbling back to handleClick + e.preventDefault(); + this.props.onMouseLeave(this.props.id); + } startRotatingIcons () { this.rotateIcon(); this.intervalId = setInterval(this.rotateIcon, 300); @@ -109,6 +116,7 @@ class LibraryItem extends React.PureComponent { id={this.props.id} insetIconURL={this.props.insetIconURL} internetConnectionRequired={this.props.internetConnectionRequired} + isPlaying={this.props.isPlaying} name={this.props.name} showPlayButton={this.props.showPlayButton} onBlur={this.handleBlur} @@ -118,6 +126,7 @@ class LibraryItem extends React.PureComponent { onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onPlay={this.handlePlay} + onStop={this.handleStop} /> ); } @@ -144,6 +153,7 @@ LibraryItem.propTypes = { id: PropTypes.number.isRequired, insetIconURL: PropTypes.string, internetConnectionRequired: PropTypes.bool, + isPlaying: PropTypes.bool, name: PropTypes.oneOfType([ PropTypes.string, PropTypes.node From 41cc11c32e6b2afc5341ef512ffb28f2da6533c2 Mon Sep 17 00:00:00 2001 From: Chris Garrity Date: Wed, 3 Jul 2019 11:02:59 -0400 Subject: [PATCH 4/8] Handle sound ending Update UI play/stop button when the sound ends --- src/components/library/library.jsx | 11 ++++++++++- src/containers/sound-library.jsx | 23 ++++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx index 0a86c26db..f2c8e770b 100644 --- a/src/components/library/library.jsx +++ b/src/components/library/library.jsx @@ -38,6 +38,7 @@ class LibraryComponent extends React.Component { 'handleFilterClear', 'handleMouseEnter', 'handleMouseLeave', + 'handlePlayingEnd', 'handleSelect', 'handleTagClick', 'setFilteredDataRef' @@ -54,6 +55,7 @@ class LibraryComponent extends React.Component { setTimeout(() => { this.setState({loaded: true}); }); + if (this.props.setStopHandler) this.props.setStopHandler(this.handlePlayingEnd); } componentDidUpdate (prevProps, prevState) { if (prevState.filterQuery !== this.state.filterQuery || @@ -75,7 +77,6 @@ class LibraryComponent extends React.Component { }); } handleMouseEnter (id) { - console.log('Library MouseEnter id:', id, ', playingItem: ', this.state.playingItem); // don't restart if mouse over already playing item if (this.props.onItemMouseEnter && this.state.playingItem !== id) { this.setState({ @@ -90,6 +91,13 @@ class LibraryComponent extends React.Component { }, this.props.onItemMouseLeave(this.getFilteredData()[id])); } } + handlePlayingEnd () { + if (this.state.playingItem !== null) { + this.setState({ + playingItem: null + }); + } + } handleFilterChange (event) { this.setState({ filterQuery: event.target.value, @@ -239,6 +247,7 @@ LibraryComponent.propTypes = { onItemMouseLeave: PropTypes.func, onItemSelected: PropTypes.func, onRequestClose: PropTypes.func, + setStopHandler: PropTypes.func, showPlayButton: PropTypes.bool, tags: PropTypes.arrayOf(PropTypes.shape(TagButton.propTypes)), title: PropTypes.string.isRequired diff --git a/src/containers/sound-library.jsx b/src/containers/sound-library.jsx index 677d93dfb..d3b9782d1 100644 --- a/src/containers/sound-library.jsx +++ b/src/containers/sound-library.jsx @@ -29,7 +29,9 @@ class SoundLibrary extends React.PureComponent { bindAll(this, [ 'handleItemSelected', 'handleItemMouseEnter', - 'handleItemMouseLeave' + 'handleItemMouseLeave', + 'onStop', + 'setStopHandler' ]); /** @@ -43,6 +45,11 @@ class SoundLibrary extends React.PureComponent { * @type {Promise} */ this.playingSoundPromise = null; + + /** + * function to call when the sound ends + */ + this.handleStop = null; } componentDidMount () { this.audioEngine = new AudioEngine(); @@ -51,10 +58,22 @@ class SoundLibrary extends React.PureComponent { componentWillUnmount () { this.stopPlayingSound(); } + onStop () { + if (this.playingSoundPromise !== null) { + this.playingSoundPromise.then(soundPlayer => soundPlayer.removeListener('stop', this.onStop)); + if (this.handleStop) this.handleStop(); + } + + } + setStopHandler (func) { + this.handleStop = func; + } stopPlayingSound () { // Playback is queued, playing, or has played recently and finished // normally. if (this.playingSoundPromise !== null) { + // Forcing sound to stop, so stop listening for sound ending: + this.playingSoundPromise.then(soundPlayer => soundPlayer.removeListener('stop', this.onStop)); // Queued playback began playing before this method. if (this.playingSoundPromise.isPlaying) { // Fetch the player from the promise and stop playback soon. @@ -102,6 +121,7 @@ class SoundLibrary extends React.PureComponent { // Play the sound. Playing the sound will always come before a // paired stop if the sound must stop early. soundPlayer.play(); + soundPlayer.addListener('stop', this.onStop); // Set that the sound is playing. This affects the type of stop // instruction given if the sound must stop early. if (this.playingSoundPromise !== null) { @@ -144,6 +164,7 @@ class SoundLibrary extends React.PureComponent { showPlayButton data={soundLibraryThumbnailData} id="soundLibrary" + setStopHandler={this.setStopHandler} tags={soundTags} title={this.props.intl.formatMessage(messages.libraryTitle)} onItemMouseEnter={this.handleItemMouseEnter} From 91f5c4910fda7a9faca0ac777eeae504d9f006bc Mon Sep 17 00:00:00 2001 From: Chris Garrity Date: Mon, 15 Jul 2019 12:19:55 -0400 Subject: [PATCH 5/8] refactor to create play-button for sound items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Play button doesn’t have a ‘touch-outside’ listener to stop sounds. That seems to be ok as other events cause the sound to stop. Both handleClick and handleMouseDown are needed. Must use handleMouseDown for `preventDefault` to skip setting focus on the item. handleClick is needed to prevent the Click from propagating to the item and selecting it. --- src/components/library-item/library-item.css | 39 ----- src/components/library-item/library-item.jsx | 26 +-- src/components/library/library.jsx | 34 +++- .../icon--play.svg | 0 .../icon--stop.svg | 0 src/components/play-button/play-button.css | 42 +++++ src/components/play-button/play-button.jsx | 148 ++++++++++++++++++ src/containers/library-item.jsx | 40 ++--- 8 files changed, 244 insertions(+), 85 deletions(-) rename src/components/{library-item => play-button}/icon--play.svg (100%) rename src/components/{library-item => play-button}/icon--stop.svg (100%) create mode 100644 src/components/play-button/play-button.css create mode 100644 src/components/play-button/play-button.jsx diff --git a/src/components/library-item/library-item.css b/src/components/library-item/library-item.css index 760153eae..79aebf796 100644 --- a/src/components/library-item/library-item.css +++ b/src/components/library-item/library-item.css @@ -201,42 +201,3 @@ [dir="rtl"] .coming-soon-text { transform: translate(calc(-2 * $space), calc(2 * $space)); } - -.play-button { - display: flex; - align-items: center; - justify-content: center; - - overflow: hidden; /* Mask the icon animation */ - width: 2.5rem; - height: 2.5rem; - background-color: $sound-primary; - color: $ui-white; - border-radius: 50%; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - user-select: none; - cursor: pointer; - transition: all 0.15s ease-out; -} - -.play-button { - position: absolute; - top: .5rem; - z-index: auto; -} - -.play-button:focus { - outline: none; -} - -.play-icon { - width: 50%; -} - -[dir="ltr"] .play-button { - left: .5rem; -} - -[dir="rtl"] .play-button { - right: .5rem; -} diff --git a/src/components/library-item/library-item.jsx b/src/components/library-item/library-item.jsx index f63f92d39..504be2b66 100644 --- a/src/components/library-item/library-item.jsx +++ b/src/components/library-item/library-item.jsx @@ -3,18 +3,12 @@ import PropTypes from 'prop-types'; import React from 'react'; import Box from '../box/box.jsx'; +import PlayButton from '../play-button/play-button.jsx'; import styles from './library-item.css'; import classNames from 'classnames'; import bluetoothIconURL from './bluetooth.svg'; import internetConnectionIconURL from './internet-connection.svg'; -import playIcon from './icon--play.svg'; -import stopIcon from './icon--stop.svg'; - -const preventClick = e => { - e.stopPropagation(); - e.preventDefault(); -}; /* eslint-disable react/prefer-stateless-function */ class LibraryItemComponent extends React.PureComponent { @@ -140,19 +134,11 @@ class LibraryItemComponent extends React.PureComponent {
{this.props.name} {this.props.showPlayButton ? ( -
- -
+ ) : null} ); diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx index f2c8e770b..259e922ff 100644 --- a/src/components/library/library.jsx +++ b/src/components/library/library.jsx @@ -71,10 +71,19 @@ class LibraryComponent extends React.Component { this.props.onRequestClose(); } handleTagClick (tag) { - this.setState({ - filterQuery: '', - selectedTag: tag.toLowerCase() - }); + if (this.state.playingItem === null) { + this.setState({ + ilterQuery: '', + selectedTag: tag.toLowerCase() + }); + } else { + const playingId = this.state.playingItem; + this.setState({ + filterQuery: '', + playingItem: null, + selectedTag: tag.toLowerCase() + }, this.props.onItemMouseLeave(this.getFilteredData()[[playingId]])); + } } handleMouseEnter (id) { // don't restart if mouse over already playing item @@ -99,10 +108,19 @@ class LibraryComponent extends React.Component { } } handleFilterChange (event) { - this.setState({ - filterQuery: event.target.value, - selectedTag: ALL_TAG.tag - }); + if (this.state.playingItem === null) { + this.setState({ + filterQuery: event.target.value, + selectedTag: ALL_TAG.tag + }); + } else { + const playingId = this.state.playingItem; + this.setState({ + filterQuery: event.target.value, + playingItem: null, + selectedTag: ALL_TAG.tag + }, this.props.onItemMouseLeave(this.getFilteredData()[[playingId]])); + } } handleFilterClear () { this.setState({filterQuery: ''}); diff --git a/src/components/library-item/icon--play.svg b/src/components/play-button/icon--play.svg similarity index 100% rename from src/components/library-item/icon--play.svg rename to src/components/play-button/icon--play.svg diff --git a/src/components/library-item/icon--stop.svg b/src/components/play-button/icon--stop.svg similarity index 100% rename from src/components/library-item/icon--stop.svg rename to src/components/play-button/icon--stop.svg diff --git a/src/components/play-button/play-button.css b/src/components/play-button/play-button.css new file mode 100644 index 000000000..fbc901e66 --- /dev/null +++ b/src/components/play-button/play-button.css @@ -0,0 +1,42 @@ + +@import "../../css/colors.css"; +@import "../../css/units.css"; + +.play-button { + display: flex; + align-items: center; + justify-content: center; + + overflow: hidden; /* Mask the icon animation */ + width: 2.5rem; + height: 2.5rem; + background-color: $sound-primary; + color: $ui-white; + border-radius: 50%; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + user-select: none; + cursor: pointer; + transition: all 0.15s ease-out; +} + +.play-button { + position: absolute; + top: .5rem; + z-index: auto; +} + +.play-button:focus { + outline: none; +} + +.play-icon { + width: 50%; +} + +[dir="ltr"] .play-button { + left: .5rem; +} + +[dir="rtl"] .play-button { + right: .5rem; +} diff --git a/src/components/play-button/play-button.jsx b/src/components/play-button/play-button.jsx new file mode 100644 index 000000000..8071ce849 --- /dev/null +++ b/src/components/play-button/play-button.jsx @@ -0,0 +1,148 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import bindAll from 'lodash.bindall'; + +import {defineMessages, injectIntl, intlShape} from 'react-intl'; + +import styles from './play-button.css'; + +import playIcon from './icon--play.svg'; +import stopIcon from './icon--stop.svg'; + +const messages = defineMessages({ + play: { + id: 'gui.playButton.play', + description: 'Title of the button to start playing the sound', + defaultMessage: 'Play' + }, + stop: { + id: 'gui.playButton.stop', + description: 'Title of the button to stop the sound', + defaultMessage: 'Stop' + } +}); + +class PlayButton extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleClick', + 'handleMouseDown', + 'handleMouseEnter', + 'handleMouseLeave', + 'handleTouchStart', + 'setButtonRef' + ]); + this.state = { + touchStarted: false + }; + } + getDerivedStateFromProps (props, state) { + // if touchStarted is true and it's not playing, the sound must have ended. + // reset the touchStarted state to allow the sound to be replayed + if (state.touchStarted && !props.isPlaying) { + return { + touchStarted: false + }; + } + return null; // nothing changed + } + componentDidMount () { + // Touch start + this.buttonRef.addEventListener('touchstart', this.handleTouchStart); + } + componentWillUnmount () { + this.buttonRef.removeEventListener('touchstart', this.handleTouchStart); + } + handleClick (e) { + // stop the click from propagating out of the button + e.stopPropagation(); + } + handleMouseDown (e) { + // prevent default (focus) on mouseDown + e.preventDefault(); + if (this.props.isPlaying) { + // stop sound and reset touch state + this.props.onStop(); + if (this.state.touchstarted) this.setState({touchStarted: false}); + } else { + this.props.onPlay(); + if (this.state.touchstarted) { + // started on touch, but now clicked mouse + this.setState({touchStarted: false}); + } + } + } + handleTouchStart (e) { + if (this.props.isPlaying) { + // If playing, stop sound, and reset touch state + e.preventDefault(); + this.setState({touchStarted: false}); + this.props.onStop(); + } else { + // otherwise start playing, and set touch state + e.preventDefault(); + this.setState({touchStarted: true}); + this.props.onPlay(); + } + } + handleMouseEnter (e) { + // start the sound if it's not already playing + e.preventDefault(); + if (!this.props.isPlaying) { + this.props.onPlay(); + } + } + handleMouseLeave () { + // stop the sound unless it was started by touch + if (this.props.isPlaying && !this.state.touchstarted) { + this.props.onStop(); + } + } + setButtonRef (ref) { + this.buttonRef = ref; + } + render () { + const { + className, + intl, + isPlaying, + onPlay, // eslint-disable-line no-unused-vars + onStop // eslint-disable-line no-unused-vars + } = this.props; + const label = isPlaying ? + intl.formatMessage(messages.stop) : + intl.formatMessage(messages.play); + + return ( +
+ +
+ ); + } +} + +PlayButton.propTypes = { + className: PropTypes.string, + intl: intlShape, + isPlaying: PropTypes.bool.isRequired, + onPlay: PropTypes.func.isRequired, + onStop: PropTypes.func.isRequired +}; + +export default injectIntl(PlayButton); diff --git a/src/containers/library-item.jsx b/src/containers/library-item.jsx index 75f6b2998..8881a57cc 100644 --- a/src/containers/library-item.jsx +++ b/src/containers/library-item.jsx @@ -39,7 +39,9 @@ class LibraryItem extends React.PureComponent { e.preventDefault(); } handleFocus (id) { - this.handleMouseEnter(id); + if (!this.props.showPlayButton) { + this.handleMouseEnter(id); + } } handleKeyPress (e) { if (e.key === ' ' || e.key === 'Enter') { @@ -48,30 +50,32 @@ class LibraryItem extends React.PureComponent { } } handleMouseEnter () { - this.props.onMouseEnter(this.props.id); - if (this.props.icons && this.props.icons.length) { - this.stopRotatingIcons(); - this.setState({ - isRotatingIcon: true - }, this.startRotatingIcons); + // only show hover effects on the item if not showing a play button + if (!this.props.showPlayButton) { + this.props.onMouseEnter(this.props.id); + if (this.props.icons && this.props.icons.length) { + this.stopRotatingIcons(); + this.setState({ + isRotatingIcon: true + }, this.startRotatingIcons); + } } } handleMouseLeave () { - this.props.onMouseLeave(this.props.id); - if (this.props.icons && this.props.icons.length) { - this.setState({ - isRotatingIcon: false - }, this.stopRotatingIcons); + // only show hover effects on the item if not showing a play button + if (!this.props.showPlayButton) { + this.props.onMouseLeave(this.props.id); + if (this.props.icons && this.props.icons.length) { + this.setState({ + isRotatingIcon: false + }, this.stopRotatingIcons); + } } } - handlePlay (e) { - e.stopPropagation(); // To prevent from bubbling back to handleClick - e.preventDefault(); + handlePlay () { this.props.onMouseEnter(this.props.id); } - handleStop (e) { - e.stopPropagation(); // To prevent from bubbling back to handleClick - e.preventDefault(); + handleStop () { this.props.onMouseLeave(this.props.id); } startRotatingIcons () { From 043e31a4728b89289cc4093d22c7eca5a35e1276 Mon Sep 17 00:00:00 2001 From: Chris Garrity Date: Wed, 31 Jul 2019 10:25:37 -0400 Subject: [PATCH 6/8] Refactor * split play-button into container and component * revised sound library tile: * play button in upper right * smaller sound icon (make the tile less loaded) --- .../library-item/lib-icon--sound-rtl.svg | 9 + .../library-item/lib-icon--sound.svg | 9 + src/components/library-item/library-item.jsx | 2 +- src/components/library/library.jsx | 2 +- src/components/play-button/play-button.css | 4 +- src/components/play-button/play-button.jsx | 159 +++++------------- src/containers/play-button.jsx | 115 +++++++++++++ src/containers/sound-library.jsx | 4 +- 8 files changed, 181 insertions(+), 123 deletions(-) create mode 100644 src/components/library-item/lib-icon--sound-rtl.svg create mode 100644 src/components/library-item/lib-icon--sound.svg create mode 100644 src/containers/play-button.jsx diff --git a/src/components/library-item/lib-icon--sound-rtl.svg b/src/components/library-item/lib-icon--sound-rtl.svg new file mode 100644 index 000000000..db193366a --- /dev/null +++ b/src/components/library-item/lib-icon--sound-rtl.svg @@ -0,0 +1,9 @@ + + + + Sound/General/Library Tile Icon/RTL + Created with Sketch. + + + + \ No newline at end of file diff --git a/src/components/library-item/lib-icon--sound.svg b/src/components/library-item/lib-icon--sound.svg new file mode 100644 index 000000000..2ffc76013 --- /dev/null +++ b/src/components/library-item/lib-icon--sound.svg @@ -0,0 +1,9 @@ + + + + Sound/General/Library Tile Icon/LTR + Created with Sketch. + + + + \ No newline at end of file diff --git a/src/components/library-item/library-item.jsx b/src/components/library-item/library-item.jsx index 504be2b66..74a65aa9a 100644 --- a/src/components/library-item/library-item.jsx +++ b/src/components/library-item/library-item.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import Box from '../box/box.jsx'; -import PlayButton from '../play-button/play-button.jsx'; +import PlayButton from '../../containers/play-button.jsx'; import styles from './library-item.css'; import classNames from 'classnames'; diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx index 259e922ff..f85def50f 100644 --- a/src/components/library/library.jsx +++ b/src/components/library/library.jsx @@ -73,7 +73,7 @@ class LibraryComponent extends React.Component { handleTagClick (tag) { if (this.state.playingItem === null) { this.setState({ - ilterQuery: '', + filterQuery: '', selectedTag: tag.toLowerCase() }); } else { diff --git a/src/components/play-button/play-button.css b/src/components/play-button/play-button.css index fbc901e66..17d2b6317 100644 --- a/src/components/play-button/play-button.css +++ b/src/components/play-button/play-button.css @@ -34,9 +34,9 @@ } [dir="ltr"] .play-button { - left: .5rem; + right: .5rem; } [dir="rtl"] .play-button { - right: .5rem; + left: .5rem; } diff --git a/src/components/play-button/play-button.jsx b/src/components/play-button/play-button.jsx index 8071ce849..15f8226ac 100644 --- a/src/components/play-button/play-button.jsx +++ b/src/components/play-button/play-button.jsx @@ -1,7 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import classNames from 'classnames'; -import bindAll from 'lodash.bindall'; import {defineMessages, injectIntl, intlShape} from 'react-intl'; @@ -23,126 +22,52 @@ const messages = defineMessages({ } }); -class PlayButton extends React.Component { - constructor (props) { - super(props); - bindAll(this, [ - 'handleClick', - 'handleMouseDown', - 'handleMouseEnter', - 'handleMouseLeave', - 'handleTouchStart', - 'setButtonRef' - ]); - this.state = { - touchStarted: false - }; - } - getDerivedStateFromProps (props, state) { - // if touchStarted is true and it's not playing, the sound must have ended. - // reset the touchStarted state to allow the sound to be replayed - if (state.touchStarted && !props.isPlaying) { - return { - touchStarted: false - }; - } - return null; // nothing changed - } - componentDidMount () { - // Touch start - this.buttonRef.addEventListener('touchstart', this.handleTouchStart); - } - componentWillUnmount () { - this.buttonRef.removeEventListener('touchstart', this.handleTouchStart); - } - handleClick (e) { - // stop the click from propagating out of the button - e.stopPropagation(); - } - handleMouseDown (e) { - // prevent default (focus) on mouseDown - e.preventDefault(); - if (this.props.isPlaying) { - // stop sound and reset touch state - this.props.onStop(); - if (this.state.touchstarted) this.setState({touchStarted: false}); - } else { - this.props.onPlay(); - if (this.state.touchstarted) { - // started on touch, but now clicked mouse - this.setState({touchStarted: false}); - } - } - } - handleTouchStart (e) { - if (this.props.isPlaying) { - // If playing, stop sound, and reset touch state - e.preventDefault(); - this.setState({touchStarted: false}); - this.props.onStop(); - } else { - // otherwise start playing, and set touch state - e.preventDefault(); - this.setState({touchStarted: true}); - this.props.onPlay(); - } - } - handleMouseEnter (e) { - // start the sound if it's not already playing - e.preventDefault(); - if (!this.props.isPlaying) { - this.props.onPlay(); - } - } - handleMouseLeave () { - // stop the sound unless it was started by touch - if (this.props.isPlaying && !this.state.touchstarted) { - this.props.onStop(); - } - } - setButtonRef (ref) { - this.buttonRef = ref; - } - render () { - const { - className, - intl, - isPlaying, - onPlay, // eslint-disable-line no-unused-vars - onStop // eslint-disable-line no-unused-vars - } = this.props; - const label = isPlaying ? - intl.formatMessage(messages.stop) : - intl.formatMessage(messages.play); +const PlayButtonComponent = ({ + className, + intl, + isPlaying, + onClick, + onMouseDown, + onMouseEnter, + onMouseLeave, + setButtonRef, + ...props +}) => { + const label = isPlaying ? + intl.formatMessage(messages.stop) : + intl.formatMessage(messages.play); - return ( -
- -
- ); - } -} + return ( +
+ +
+ ); +}; -PlayButton.propTypes = { +PlayButtonComponent.propTypes = { className: PropTypes.string, intl: intlShape, isPlaying: PropTypes.bool.isRequired, - onPlay: PropTypes.func.isRequired, - onStop: PropTypes.func.isRequired + onClick: PropTypes.func.isRequired, + onMouseDown: PropTypes.func.isRequired, + onMouseEnter: PropTypes.func.isRequired, + onMouseLeave: PropTypes.func.isRequired, + setButtonRef: PropTypes.func.isRequired }; -export default injectIntl(PlayButton); +export default injectIntl(PlayButtonComponent); diff --git a/src/containers/play-button.jsx b/src/containers/play-button.jsx new file mode 100644 index 000000000..f0ed8b6a2 --- /dev/null +++ b/src/containers/play-button.jsx @@ -0,0 +1,115 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import bindAll from 'lodash.bindall'; + +import PlayButtonComponent from '../components/play-button/play-button.jsx'; + +class PlayButton extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleClick', + 'handleMouseDown', + 'handleMouseEnter', + 'handleMouseLeave', + 'handleTouchStart', + 'setButtonRef' + ]); + this.state = { + touchStarted: false + }; + } + getDerivedStateFromProps (props, state) { + // if touchStarted is true and it's not playing, the sound must have ended. + // reset the touchStarted state to allow the sound to be replayed + if (state.touchStarted && !props.isPlaying) { + return { + touchStarted: false + }; + } + return null; // nothing changed + } + componentDidMount () { + // Touch start + this.buttonRef.addEventListener('touchstart', this.handleTouchStart); + } + componentWillUnmount () { + this.buttonRef.removeEventListener('touchstart', this.handleTouchStart); + } + handleClick (e) { + // stop the click from propagating out of the button + e.stopPropagation(); + } + handleMouseDown (e) { + // prevent default (focus) on mouseDown + e.preventDefault(); + if (this.props.isPlaying) { + // stop sound and reset touch state + this.props.onStop(); + if (this.state.touchstarted) this.setState({touchStarted: false}); + } else { + this.props.onPlay(); + if (this.state.touchstarted) { + // started on touch, but now clicked mouse + this.setState({touchStarted: false}); + } + } + } + handleTouchStart (e) { + if (this.props.isPlaying) { + // If playing, stop sound, and reset touch state + e.preventDefault(); + this.setState({touchStarted: false}); + this.props.onStop(); + } else { + // otherwise start playing, and set touch state + e.preventDefault(); + this.setState({touchStarted: true}); + this.props.onPlay(); + } + } + handleMouseEnter (e) { + // start the sound if it's not already playing + e.preventDefault(); + if (!this.props.isPlaying) { + this.props.onPlay(); + } + } + handleMouseLeave () { + // stop the sound unless it was started by touch + if (this.props.isPlaying && !this.state.touchstarted) { + this.props.onStop(); + } + } + setButtonRef (ref) { + this.buttonRef = ref; + } + render () { + const { + className, + isPlaying, + onPlay, // eslint-disable-line no-unused-vars + onStop // eslint-disable-line no-unused-vars + } = this.props; + return ( + + ); + } +} + +PlayButton.propTypes = { + className: PropTypes.string, + isPlaying: PropTypes.bool.isRequired, + onPlay: PropTypes.func.isRequired, + onStop: PropTypes.func.isRequired +}; + +export default PlayButton; diff --git a/src/containers/sound-library.jsx b/src/containers/sound-library.jsx index d3b9782d1..cdb5c7c6a 100644 --- a/src/containers/sound-library.jsx +++ b/src/containers/sound-library.jsx @@ -7,8 +7,8 @@ import AudioEngine from 'scratch-audio'; import LibraryComponent from '../components/library/library.jsx'; -import soundIcon from '../components/asset-panel/icon--sound.svg'; -import soundIconRtl from '../components/asset-panel/icon--sound-rtl.svg'; +import soundIcon from '../components/library-item/lib-icon--sound.svg'; +import soundIconRtl from '../components/library-item/lib-icon--sound-rtl.svg'; import soundLibraryContent from '../lib/libraries/sounds.json'; import soundTags from '../lib/libraries/sound-tags'; From ab1537dab4205bc83a34e97298e5c015c5083735 Mon Sep 17 00:00:00 2001 From: Chris Garrity Date: Wed, 31 Jul 2019 10:34:42 -0400 Subject: [PATCH 7/8] Use classNames instead of ternary Use the classNames feature to include/exclude a class based on a boolean instead of ternary operator --- src/components/library-item/library-item.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/library-item/library-item.jsx b/src/components/library-item/library-item.jsx index 74a65aa9a..81eac4801 100644 --- a/src/components/library-item/library-item.jsx +++ b/src/components/library-item/library-item.jsx @@ -107,8 +107,9 @@ class LibraryItemComponent extends React.PureComponent { ) : ( Date: Wed, 31 Jul 2019 15:37:50 -0400 Subject: [PATCH 8/8] remove callbacks --- src/components/library/library.jsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx index f85def50f..1272cb4a1 100644 --- a/src/components/library/library.jsx +++ b/src/components/library/library.jsx @@ -77,27 +77,29 @@ class LibraryComponent extends React.Component { selectedTag: tag.toLowerCase() }); } else { - const playingId = this.state.playingItem; + this.props.onItemMouseLeave(this.getFilteredData()[[this.state.playingItem]]); this.setState({ filterQuery: '', playingItem: null, selectedTag: tag.toLowerCase() - }, this.props.onItemMouseLeave(this.getFilteredData()[[playingId]])); + }); } } handleMouseEnter (id) { // don't restart if mouse over already playing item if (this.props.onItemMouseEnter && this.state.playingItem !== id) { + this.props.onItemMouseEnter(this.getFilteredData()[id]); this.setState({ playingItem: id - }, this.props.onItemMouseEnter(this.getFilteredData()[id])); + }); } } handleMouseLeave (id) { if (this.props.onItemMouseLeave) { + this.props.onItemMouseLeave(this.getFilteredData()[id]); this.setState({ playingItem: null - }, this.props.onItemMouseLeave(this.getFilteredData()[id])); + }); } } handlePlayingEnd () { @@ -114,12 +116,12 @@ class LibraryComponent extends React.Component { selectedTag: ALL_TAG.tag }); } else { - const playingId = this.state.playingItem; + this.props.onItemMouseLeave(this.getFilteredData()[[this.state.playingItem]]); this.setState({ filterQuery: event.target.value, playingItem: null, selectedTag: ALL_TAG.tag - }, this.props.onItemMouseLeave(this.getFilteredData()[[playingId]])); + }); } } handleFilterClear () {