Skip to content
Snippets Groups Projects
Unverified Commit 69b7eef7 authored by chrisgarrity's avatar chrisgarrity Committed by GitHub
Browse files

Merge pull request #4926 from LLK/sound-lib-touch

Add play buttons to sound library tiles for touch
parents 3feff9fe e2c2bd39
No related branches found
No related tags found
No related merge requests found
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
......@@ -7,6 +7,7 @@
align-items: center;
justify-content: flex-start;
flex-basis: 160px;
position: relative;
height: 160px;
max-width: 160px;
margin: $space;
......
......@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import Box from '../box/box.jsx';
import PlayButton from '../../containers/play-button.jsx';
import styles from './library-item.css';
import classNames from 'classnames';
......@@ -106,8 +107,9 @@ class LibraryItemComponent extends React.PureComponent {
) : (
<Box
className={classNames(
styles.libraryItem,
this.props.hidden ? styles.hidden : null
styles.libraryItem, {
[styles.hidden]: this.props.hidden
}
)}
role="button"
tabIndex="0"
......@@ -115,12 +117,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 */}
<Box className={styles.libraryItemImageContainerWrapper}>
<Box className={styles.libraryItemImageContainer}>
<Box
className={styles.libraryItemImageContainer}
onMouseEnter={this.props.showPlayButton ? this.props.onMouseEnter : null}
onMouseLeave={this.props.showPlayButton ? this.props.onMouseLeave : null}
>
<img
className={styles.libraryItemImage}
src={this.props.iconURL}
......@@ -128,6 +134,13 @@ class LibraryItemComponent extends React.PureComponent {
</Box>
</Box>
<span className={styles.libraryItemName}>{this.props.name}</span>
{this.props.showPlayButton ? (
<PlayButton
isPlaying={this.props.isPlaying}
onPlay={this.props.onPlay}
onStop={this.props.onStop}
/>
) : null}
</Box>
);
}
......@@ -149,6 +162,7 @@ LibraryItemComponent.propTypes = {
iconURL: PropTypes.string,
insetIconURL: PropTypes.string,
internetConnectionRequired: PropTypes.bool,
isPlaying: PropTypes.bool,
name: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node
......@@ -158,11 +172,15 @@ 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,
onStop: PropTypes.func.isRequired,
showPlayButton: PropTypes.bool
};
LibraryItemComponent.defaultProps = {
disabled: false
disabled: false,
showPlayButton: false
};
export default LibraryItemComponent;
......@@ -38,12 +38,13 @@ class LibraryComponent extends React.Component {
'handleFilterClear',
'handleMouseEnter',
'handleMouseLeave',
'handlePlayingEnd',
'handleSelect',
'handleTagClick',
'setFilteredDataRef'
]);
this.state = {
selectedItem: null,
playingItem: null,
filterQuery: '',
selectedTag: ALL_TAG.tag,
loaded: false
......@@ -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 ||
......@@ -69,22 +71,58 @@ class LibraryComponent extends React.Component {
this.props.onRequestClose();
}
handleTagClick (tag) {
this.setState({
filterQuery: '',
selectedTag: tag.toLowerCase()
});
if (this.state.playingItem === null) {
this.setState({
filterQuery: '',
selectedTag: tag.toLowerCase()
});
} else {
this.props.onItemMouseLeave(this.getFilteredData()[[this.state.playingItem]]);
this.setState({
filterQuery: '',
playingItem: null,
selectedTag: tag.toLowerCase()
});
}
}
handleMouseEnter (id) {
if (this.props.onItemMouseEnter) this.props.onItemMouseEnter(this.getFilteredData()[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
});
}
}
handleMouseLeave (id) {
if (this.props.onItemMouseLeave) this.props.onItemMouseLeave(this.getFilteredData()[id]);
if (this.props.onItemMouseLeave) {
this.props.onItemMouseLeave(this.getFilteredData()[id]);
this.setState({
playingItem: null
});
}
}
handlePlayingEnd () {
if (this.state.playingItem !== null) {
this.setState({
playingItem: null
});
}
}
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 {
this.props.onItemMouseLeave(this.getFilteredData()[[this.state.playingItem]]);
this.setState({
filterQuery: event.target.value,
playingItem: null,
selectedTag: ALL_TAG.tag
});
}
}
handleFilterClear () {
this.setState({filterQuery: ''});
......@@ -185,8 +223,10 @@ 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}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onSelect={this.handleSelect}
......@@ -227,12 +267,15 @@ 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
};
LibraryComponent.defaultProps = {
filterable: true
filterable: true,
showPlayButton: false
};
export default injectIntl(LibraryComponent);
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
@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 {
right: .5rem;
}
[dir="rtl"] .play-button {
left: .5rem;
}
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
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'
}
});
const PlayButtonComponent = ({
className,
intl,
isPlaying,
onClick,
onMouseDown,
onMouseEnter,
onMouseLeave,
setButtonRef,
...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
})}
onClick={onClick}
onMouseDown={onMouseDown}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
ref={setButtonRef}
{...props}
>
<img
className={styles.playIcon}
draggable={false}
src={isPlaying ? stopIcon : playIcon}
/>
</div>
);
};
PlayButtonComponent.propTypes = {
className: PropTypes.string,
intl: intlShape,
isPlaying: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
onMouseDown: PropTypes.func.isRequired,
onMouseEnter: PropTypes.func.isRequired,
onMouseLeave: PropTypes.func.isRequired,
setButtonRef: PropTypes.func.isRequired
};
export default injectIntl(PlayButtonComponent);
......@@ -15,6 +15,8 @@ class LibraryItem extends React.PureComponent {
'handleKeyPress',
'handleMouseEnter',
'handleMouseLeave',
'handlePlay',
'handleStop',
'rotateIcon',
'startRotatingIcons',
'stopRotatingIcons'
......@@ -37,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') {
......@@ -46,22 +50,34 @@ 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 () {
this.props.onMouseEnter(this.props.id);
}
handleStop () {
this.props.onMouseLeave(this.props.id);
}
startRotatingIcons () {
this.rotateIcon();
this.intervalId = setInterval(this.rotateIcon, 300);
......@@ -104,13 +120,17 @@ 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}
onClick={this.handleClick}
onFocus={this.handleFocus}
onKeyPress={this.handleKeyPress}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onPlay={this.handlePlay}
onStop={this.handleStop}
/>
);
}
......@@ -137,13 +157,15 @@ LibraryItem.propTypes = {
id: PropTypes.number.isRequired,
insetIconURL: PropTypes.string,
internetConnectionRequired: PropTypes.bool,
isPlaying: PropTypes.bool,
name: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node
]),
onMouseEnter: PropTypes.func.isRequired,
onMouseLeave: PropTypes.func.isRequired,
onSelect: PropTypes.func.isRequired
onSelect: PropTypes.func.isRequired,
showPlayButton: PropTypes.bool
};
export default injectIntl(LibraryItem);
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 (
<PlayButtonComponent
className={className}
isPlaying={isPlaying}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
setButtonRef={this.setButtonRef}
/>
);
}
}
PlayButton.propTypes = {
className: PropTypes.string,
isPlaying: PropTypes.bool.isRequired,
onPlay: PropTypes.func.isRequired,
onStop: PropTypes.func.isRequired
};
export default PlayButton;
......@@ -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';
......@@ -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<SoundPlayer>}
*/
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) {
......@@ -141,8 +161,10 @@ class SoundLibrary extends React.PureComponent {
return (
<LibraryComponent
showPlayButton
data={soundLibraryThumbnailData}
id="soundLibrary"
setStopHandler={this.setStopHandler}
tags={soundTags}
title={this.props.intl.formatMessage(messages.libraryTitle)}
onItemMouseEnter={this.handleItemMouseEnter}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment