refactor to create play-button for sound items

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.
This commit is contained in:
Chris Garrity committed 2019-07-31 12:05:16 -04:00
1 parent 41cc11c32e
commit 91f5c4910f
8 files changed
+244 -85

No files matched your search

@@ -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;
}
+6 -20
View File
@@ -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>
);
+26 -8
View File
@@ -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: ''});
@@ -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;
}
+148
View File
@@ -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);
+22 -18
View File
@@ -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 () {