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:
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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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: ''});
|
||||
|
||||
File renamed without changes.
File renamed without changes.
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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 () {
|
||||
|
||||
Reference in new issue
Block a user