Skip to content

Commit 91f5c49

Browse files
committed
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.
1 parent 41cc11c commit 91f5c49

File tree

8 files changed

+244
-85
lines changed

8 files changed

+244
-85
lines changed

src/components/library-item/library-item.css

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -201,42 +201,3 @@
201201
[dir="rtl"] .coming-soon-text {
202202
transform: translate(calc(-2 * $space), calc(2 * $space));
203203
}
204-
205-
.play-button {
206-
display: flex;
207-
align-items: center;
208-
justify-content: center;
209-
210-
overflow: hidden; /* Mask the icon animation */
211-
width: 2.5rem;
212-
height: 2.5rem;
213-
background-color: $sound-primary;
214-
color: $ui-white;
215-
border-radius: 50%;
216-
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
217-
user-select: none;
218-
cursor: pointer;
219-
transition: all 0.15s ease-out;
220-
}
221-
222-
.play-button {
223-
position: absolute;
224-
top: .5rem;
225-
z-index: auto;
226-
}
227-
228-
.play-button:focus {
229-
outline: none;
230-
}
231-
232-
.play-icon {
233-
width: 50%;
234-
}
235-
236-
[dir="ltr"] .play-button {
237-
left: .5rem;
238-
}
239-
240-
[dir="rtl"] .play-button {
241-
right: .5rem;
242-
}

src/components/library-item/library-item.jsx

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,12 @@ import PropTypes from 'prop-types';
33
import React from 'react';
44

55
import Box from '../box/box.jsx';
6+
import PlayButton from '../play-button/play-button.jsx';
67
import styles from './library-item.css';
78
import classNames from 'classnames';
89

910
import bluetoothIconURL from './bluetooth.svg';
1011
import internetConnectionIconURL from './internet-connection.svg';
11-
import playIcon from './icon--play.svg';
12-
import stopIcon from './icon--stop.svg';
13-
14-
const preventClick = e => {
15-
e.stopPropagation();
16-
e.preventDefault();
17-
};
1812

1913
/* eslint-disable react/prefer-stateless-function */
2014
class LibraryItemComponent extends React.PureComponent {
@@ -140,19 +134,11 @@ class LibraryItemComponent extends React.PureComponent {
140134
</Box>
141135
<span className={styles.libraryItemName}>{this.props.name}</span>
142136
{this.props.showPlayButton ? (
143-
<div
144-
aria-label="Play"
145-
className={styles.playButton}
146-
onClick={preventClick}
147-
onMouseDown={this.props.isPlaying ? this.props.onStop : this.props.onPlay}
148-
onMouseLeave={this.props.isPlaying ? this.props.onStop : null}
149-
>
150-
<img
151-
className={styles.playIcon}
152-
draggable={false}
153-
src={this.props.isPlaying ? stopIcon : playIcon}
154-
/>
155-
</div>
137+
<PlayButton
138+
isPlaying={this.props.isPlaying}
139+
onPlay={this.props.onPlay}
140+
onStop={this.props.onStop}
141+
/>
156142
) : null}
157143
</Box>
158144
);

src/components/library/library.jsx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,19 @@ class LibraryComponent extends React.Component {
7171
this.props.onRequestClose();
7272
}
7373
handleTagClick (tag) {
74-
this.setState({
75-
filterQuery: '',
76-
selectedTag: tag.toLowerCase()
77-
});
74+
if (this.state.playingItem === null) {
75+
this.setState({
76+
ilterQuery: '',
77+
selectedTag: tag.toLowerCase()
78+
});
79+
} else {
80+
const playingId = this.state.playingItem;
81+
this.setState({
82+
filterQuery: '',
83+
playingItem: null,
84+
selectedTag: tag.toLowerCase()
85+
}, this.props.onItemMouseLeave(this.getFilteredData()[[playingId]]));
86+
}
7887
}
7988
handleMouseEnter (id) {
8089
// don't restart if mouse over already playing item
@@ -99,10 +108,19 @@ class LibraryComponent extends React.Component {
99108
}
100109
}
101110
handleFilterChange (event) {
102-
this.setState({
103-
filterQuery: event.target.value,
104-
selectedTag: ALL_TAG.tag
105-
});
111+
if (this.state.playingItem === null) {
112+
this.setState({
113+
filterQuery: event.target.value,
114+
selectedTag: ALL_TAG.tag
115+
});
116+
} else {
117+
const playingId = this.state.playingItem;
118+
this.setState({
119+
filterQuery: event.target.value,
120+
playingItem: null,
121+
selectedTag: ALL_TAG.tag
122+
}, this.props.onItemMouseLeave(this.getFilteredData()[[playingId]]));
123+
}
106124
}
107125
handleFilterClear () {
108126
this.setState({filterQuery: ''});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
2+
@import "../../css/colors.css";
3+
@import "../../css/units.css";
4+
5+
.play-button {
6+
display: flex;
7+
align-items: center;
8+
justify-content: center;
9+
10+
overflow: hidden; /* Mask the icon animation */
11+
width: 2.5rem;
12+
height: 2.5rem;
13+
background-color: $sound-primary;
14+
color: $ui-white;
15+
border-radius: 50%;
16+
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
17+
user-select: none;
18+
cursor: pointer;
19+
transition: all 0.15s ease-out;
20+
}
21+
22+
.play-button {
23+
position: absolute;
24+
top: .5rem;
25+
z-index: auto;
26+
}
27+
28+
.play-button:focus {
29+
outline: none;
30+
}
31+
32+
.play-icon {
33+
width: 50%;
34+
}
35+
36+
[dir="ltr"] .play-button {
37+
left: .5rem;
38+
}
39+
40+
[dir="rtl"] .play-button {
41+
right: .5rem;
42+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import PropTypes from 'prop-types';
2+
import React from 'react';
3+
import classNames from 'classnames';
4+
import bindAll from 'lodash.bindall';
5+
6+
import {defineMessages, injectIntl, intlShape} from 'react-intl';
7+
8+
import styles from './play-button.css';
9+
10+
import playIcon from './icon--play.svg';
11+
import stopIcon from './icon--stop.svg';
12+
13+
const messages = defineMessages({
14+
play: {
15+
id: 'gui.playButton.play',
16+
description: 'Title of the button to start playing the sound',
17+
defaultMessage: 'Play'
18+
},
19+
stop: {
20+
id: 'gui.playButton.stop',
21+
description: 'Title of the button to stop the sound',
22+
defaultMessage: 'Stop'
23+
}
24+
});
25+
26+
class PlayButton extends React.Component {
27+
constructor (props) {
28+
super(props);
29+
bindAll(this, [
30+
'handleClick',
31+
'handleMouseDown',
32+
'handleMouseEnter',
33+
'handleMouseLeave',
34+
'handleTouchStart',
35+
'setButtonRef'
36+
]);
37+
this.state = {
38+
touchStarted: false
39+
};
40+
}
41+
getDerivedStateFromProps (props, state) {
42+
// if touchStarted is true and it's not playing, the sound must have ended.
43+
// reset the touchStarted state to allow the sound to be replayed
44+
if (state.touchStarted && !props.isPlaying) {
45+
return {
46+
touchStarted: false
47+
};
48+
}
49+
return null; // nothing changed
50+
}
51+
componentDidMount () {
52+
// Touch start
53+
this.buttonRef.addEventListener('touchstart', this.handleTouchStart);
54+
}
55+
componentWillUnmount () {
56+
this.buttonRef.removeEventListener('touchstart', this.handleTouchStart);
57+
}
58+
handleClick (e) {
59+
// stop the click from propagating out of the button
60+
e.stopPropagation();
61+
}
62+
handleMouseDown (e) {
63+
// prevent default (focus) on mouseDown
64+
e.preventDefault();
65+
if (this.props.isPlaying) {
66+
// stop sound and reset touch state
67+
this.props.onStop();
68+
if (this.state.touchstarted) this.setState({touchStarted: false});
69+
} else {
70+
this.props.onPlay();
71+
if (this.state.touchstarted) {
72+
// started on touch, but now clicked mouse
73+
this.setState({touchStarted: false});
74+
}
75+
}
76+
}
77+
handleTouchStart (e) {
78+
if (this.props.isPlaying) {
79+
// If playing, stop sound, and reset touch state
80+
e.preventDefault();
81+
this.setState({touchStarted: false});
82+
this.props.onStop();
83+
} else {
84+
// otherwise start playing, and set touch state
85+
e.preventDefault();
86+
this.setState({touchStarted: true});
87+
this.props.onPlay();
88+
}
89+
}
90+
handleMouseEnter (e) {
91+
// start the sound if it's not already playing
92+
e.preventDefault();
93+
if (!this.props.isPlaying) {
94+
this.props.onPlay();
95+
}
96+
}
97+
handleMouseLeave () {
98+
// stop the sound unless it was started by touch
99+
if (this.props.isPlaying && !this.state.touchstarted) {
100+
this.props.onStop();
101+
}
102+
}
103+
setButtonRef (ref) {
104+
this.buttonRef = ref;
105+
}
106+
render () {
107+
const {
108+
className,
109+
intl,
110+
isPlaying,
111+
onPlay, // eslint-disable-line no-unused-vars
112+
onStop // eslint-disable-line no-unused-vars
113+
} = this.props;
114+
const label = isPlaying ?
115+
intl.formatMessage(messages.stop) :
116+
intl.formatMessage(messages.play);
117+
118+
return (
119+
<div
120+
aria-label={label}
121+
className={classNames(styles.playButton, className, {
122+
[styles.playing]: isPlaying
123+
})}
124+
ref={this.setButtonRef}
125+
onClick={this.handleClick}
126+
onMouseDown={this.handleMouseDown}
127+
onMouseEnter={this.handleMouseEnter}
128+
onMouseLeave={this.handleMouseLeave}
129+
>
130+
<img
131+
className={styles.playIcon}
132+
draggable={false}
133+
src={isPlaying ? stopIcon : playIcon}
134+
/>
135+
</div>
136+
);
137+
}
138+
}
139+
140+
PlayButton.propTypes = {
141+
className: PropTypes.string,
142+
intl: intlShape,
143+
isPlaying: PropTypes.bool.isRequired,
144+
onPlay: PropTypes.func.isRequired,
145+
onStop: PropTypes.func.isRequired
146+
};
147+
148+
export default injectIntl(PlayButton);

src/containers/library-item.jsx

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ class LibraryItem extends React.PureComponent {
3939
e.preventDefault();
4040
}
4141
handleFocus (id) {
42-
this.handleMouseEnter(id);
42+
if (!this.props.showPlayButton) {
43+
this.handleMouseEnter(id);
44+
}
4345
}
4446
handleKeyPress (e) {
4547
if (e.key === ' ' || e.key === 'Enter') {
@@ -48,30 +50,32 @@ class LibraryItem extends React.PureComponent {
4850
}
4951
}
5052
handleMouseEnter () {
51-
this.props.onMouseEnter(this.props.id);
52-
if (this.props.icons && this.props.icons.length) {
53-
this.stopRotatingIcons();
54-
this.setState({
55-
isRotatingIcon: true
56-
}, this.startRotatingIcons);
53+
// only show hover effects on the item if not showing a play button
54+
if (!this.props.showPlayButton) {
55+
this.props.onMouseEnter(this.props.id);
56+
if (this.props.icons && this.props.icons.length) {
57+
this.stopRotatingIcons();
58+
this.setState({
59+
isRotatingIcon: true
60+
}, this.startRotatingIcons);
61+
}
5762
}
5863
}
5964
handleMouseLeave () {
60-
this.props.onMouseLeave(this.props.id);
61-
if (this.props.icons && this.props.icons.length) {
62-
this.setState({
63-
isRotatingIcon: false
64-
}, this.stopRotatingIcons);
65+
// only show hover effects on the item if not showing a play button
66+
if (!this.props.showPlayButton) {
67+
this.props.onMouseLeave(this.props.id);
68+
if (this.props.icons && this.props.icons.length) {
69+
this.setState({
70+
isRotatingIcon: false
71+
}, this.stopRotatingIcons);
72+
}
6573
}
6674
}
67-
handlePlay (e) {
68-
e.stopPropagation(); // To prevent from bubbling back to handleClick
69-
e.preventDefault();
75+
handlePlay () {
7076
this.props.onMouseEnter(this.props.id);
7177
}
72-
handleStop (e) {
73-
e.stopPropagation(); // To prevent from bubbling back to handleClick
74-
e.preventDefault();
78+
handleStop () {
7579
this.props.onMouseLeave(this.props.id);
7680
}
7781
startRotatingIcons () {

0 commit comments

Comments
 (0)