diff --git a/src/components/library-item/library-item.css b/src/components/library-item/library-item.css index 760153eaedebf041aca9d35e60bf9450056ce8de..79aebf79693435b0183591f64c6797967f1fb07d 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 f63f92d39e45bc30fff1902fc306ef558990abc0..504be2b66949531daf7e829f5808ab2fa04f8e33 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 { </Box> <span className={styles.libraryItemName}>{this.props.name}</span> {this.props.showPlayButton ? ( - <div - aria-label="Play" - className={styles.playButton} - onClick={preventClick} - onMouseDown={this.props.isPlaying ? this.props.onStop : this.props.onPlay} - onMouseLeave={this.props.isPlaying ? this.props.onStop : null} - > - <img - className={styles.playIcon} - draggable={false} - src={this.props.isPlaying ? stopIcon : playIcon} - /> - </div> + <PlayButton + isPlaying={this.props.isPlaying} + onPlay={this.props.onPlay} + onStop={this.props.onStop} + /> ) : null} </Box> ); diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx index f2c8e770b2e6d7069cfae5546a13cf68fd49c654..259e922ff3bcc03b880a037de7030b2b1fe9ed24 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 0000000000000000000000000000000000000000..fbc901e66ca32b8c54911315bfedc270ac068fd8 --- /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 0000000000000000000000000000000000000000..8071ce849c62b349417be8b305f8f7cd9bd8be17 --- /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 ( + <div + aria-label={label} + className={classNames(styles.playButton, className, { + [styles.playing]: isPlaying + })} + ref={this.setButtonRef} + onClick={this.handleClick} + onMouseDown={this.handleMouseDown} + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} + > + <img + className={styles.playIcon} + draggable={false} + src={isPlaying ? stopIcon : playIcon} + /> + </div> + ); + } +} + +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 75f6b2998ea7ec67ebbe913bd32193952f605871..8881a57cc0a77eedd12613469dab3a4b4621c704 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 () {