From 893beb5a15783a4ec31832d3087166335f5b6c4e Mon Sep 17 00:00:00 2001 From: Ben Wheeler <wheeler.benjamin@gmail.com> Date: Thu, 13 Dec 2018 21:57:12 -0500 Subject: [PATCH] added library-item container, moved costume rotation to library items --- src/components/library-item/library-item.jsx | 34 ++--- src/components/library/library.jsx | 63 ++++----- src/containers/library-item.jsx | 134 +++++++++++++++++++ src/containers/sprite-library.jsx | 50 +------ test/integration/costumes.test.js | 17 +++ 5 files changed, 187 insertions(+), 111 deletions(-) create mode 100644 src/containers/library-item.jsx diff --git a/src/components/library-item/library-item.jsx b/src/components/library-item/library-item.jsx index 03a9d6b78..c9ba6ea55 100644 --- a/src/components/library-item/library-item.jsx +++ b/src/components/library-item/library-item.jsx @@ -10,24 +10,14 @@ import classNames from 'classnames'; import bluetoothIconURL from './bluetooth.svg'; import internetConnectionIconURL from './internet-connection.svg'; -class LibraryItem extends React.PureComponent { +class LibraryItemComponent extends React.PureComponent { constructor (props) { super(props); bindAll(this, [ - 'handleBlur', 'handleClick', - 'handleFocus', - 'handleKeyPress', - 'handleMouseEnter', - 'handleMouseLeave' + 'handleKeyPress' ]); } - handleBlur () { - this.props.onBlur(this.props.id); - } - handleFocus () { - this.props.onFocus(this.props.id); - } handleClick (e) { if (!this.props.disabled) { this.props.onSelect(this.props.id); @@ -40,12 +30,6 @@ class LibraryItem extends React.PureComponent { this.props.onSelect(this.props.id); } } - handleMouseEnter () { - this.props.onMouseEnter(this.props.id); - } - handleMouseLeave () { - this.props.onMouseLeave(this.props.id); - } render () { return this.props.featured ? ( <div @@ -146,12 +130,12 @@ class LibraryItem extends React.PureComponent { )} role="button" tabIndex="0" - onBlur={this.handleBlur} + onBlur={this.props.onBlur} onClick={this.handleClick} - onFocus={this.handleFocus} + onFocus={this.props.onFocus} onKeyPress={this.handleKeyPress} - onMouseEnter={this.handleMouseEnter} - onMouseLeave={this.handleMouseLeave} + onMouseEnter={this.props.onMouseEnter} + onMouseLeave={this.props.onMouseLeave} > {/* Layers of wrapping is to prevent layout thrashing on animation */} <Box className={styles.libraryItemImageContainerWrapper}> @@ -168,7 +152,7 @@ class LibraryItem extends React.PureComponent { } } -LibraryItem.propTypes = { +LibraryItemComponent.propTypes = { bluetoothRequired: PropTypes.bool, collaborator: PropTypes.string, description: PropTypes.oneOfType([ @@ -194,8 +178,8 @@ LibraryItem.propTypes = { onSelect: PropTypes.func.isRequired }; -LibraryItem.defaultProps = { +LibraryItemComponent.defaultProps = { disabled: false }; -export default LibraryItem; +export default LibraryItemComponent; diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx index 8ecb76181..9e43ddd6a 100644 --- a/src/components/library/library.jsx +++ b/src/components/library/library.jsx @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import {defineMessages, injectIntl, intlShape} from 'react-intl'; -import LibraryItem from '../library-item/library-item.jsx'; +import LibraryItem from '../../containers/library-item.jsx'; import Modal from '../../containers/modal.jsx'; import Divider from '../divider/divider.jsx'; import Filter from '../filter/filter.jsx'; @@ -33,11 +33,9 @@ class LibraryComponent extends React.Component { constructor (props) { super(props); bindAll(this, [ - 'handleBlur', 'handleClose', 'handleFilterChange', 'handleFilterClear', - 'handleFocus', 'handleMouseEnter', 'handleMouseLeave', 'handleSelect', @@ -56,12 +54,6 @@ class LibraryComponent extends React.Component { this.scrollToTop(); } } - handleBlur (id) { - this.handleMouseLeave(id); - } - handleFocus (id) { - this.handleMouseEnter(id); - } handleSelect (id) { this.handleClose(); this.props.onItemSelected(this.getFilteredData()[id]); @@ -76,10 +68,10 @@ class LibraryComponent extends React.Component { selectedTag: tag.toLowerCase() }); } - handleMouseEnter (id) { + handleMouseEnter (id) { // no longer used for sprite costume switching, only for other libraries if (this.props.onItemMouseEnter) this.props.onItemMouseEnter(this.getFilteredData()[id]); } - handleMouseLeave (id) { + handleMouseLeave (id) { // no longer used for sprite costume switching, only for other libraries if (this.props.onItemMouseLeave) this.props.onItemMouseLeave(this.getFilteredData()[id]); } handleFilterChange (event) { @@ -172,33 +164,28 @@ class LibraryComponent extends React.Component { })} ref={this.setFilteredDataRef} > - {this.getFilteredData().map((dataItem, index) => { - const scratchURL = dataItem.md5 ? - `https://cdn.assets.scratch.mit.edu/internalapi/asset/${dataItem.md5}/get/` : - dataItem.rawURL; - return ( - <LibraryItem - bluetoothRequired={dataItem.bluetoothRequired} - collaborator={dataItem.collaborator} - description={dataItem.description} - disabled={dataItem.disabled} - extensionId={dataItem.extensionId} - featured={dataItem.featured} - hidden={dataItem.hidden} - iconURL={scratchURL} - id={index} - insetIconURL={dataItem.insetIconURL} - internetConnectionRequired={dataItem.internetConnectionRequired} - key={`item_${index}`} - name={dataItem.name} - onBlur={this.handleBlur} - onFocus={this.handleFocus} - onMouseEnter={this.handleMouseEnter} - onMouseLeave={this.handleMouseLeave} - onSelect={this.handleSelect} - /> - ); - })} + {this.getFilteredData().map((dataItem, index) => ( + <LibraryItem + bluetoothRequired={dataItem.bluetoothRequired} + collaborator={dataItem.collaborator} + description={dataItem.description} + disabled={dataItem.disabled} + extensionId={dataItem.extensionId} + featured={dataItem.featured} + hidden={dataItem.hidden} + iconMd5={dataItem.md5} + iconRawURL={dataItem.rawURL} + icons={dataItem.json && dataItem.json.costumes} + id={index} + insetIconURL={dataItem.insetIconURL} + internetConnectionRequired={dataItem.internetConnectionRequired} + key={`item_${index}`} + name={dataItem.name} + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} + onSelect={this.handleSelect} + /> + ))} </div> </Modal> ); diff --git a/src/containers/library-item.jsx b/src/containers/library-item.jsx new file mode 100644 index 000000000..7f81b4389 --- /dev/null +++ b/src/containers/library-item.jsx @@ -0,0 +1,134 @@ +import bindAll from 'lodash.bindall'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {injectIntl} from 'react-intl'; + +import LibraryItemComponent from '../components/library-item/library-item.jsx'; + +class LibraryItem extends React.PureComponent { + constructor (props) { + super(props); + bindAll(this, [ + 'handleBlur', + 'handleFocus', + 'handleMouseEnter', + 'handleMouseLeave', + 'rotateIcon', + 'startRotatingIcons', + 'stopRotatingIcons' + ]); + this.state = { + iconIndex: 0, + isRotatingIcon: false + }; + } + componentWillUnmount () { + clearInterval(this.intervalId); + } + handleBlur (id) { + this.handleMouseLeave(id); + } + handleFocus (id) { + this.handleMouseEnter(id); + } + handleMouseEnter () { + 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); + } + } + startRotatingIcons () { + this.rotateIcon(); + this.intervalId = setInterval(this.rotateIcon, 300); + } + stopRotatingIcons () { + if (this.intervalId) { + this.intervalId = clearInterval(this.intervalId); + } + } + rotateIcon () { + const nextIconIndex = (this.state.iconIndex + 1) % this.props.icons.length; + this.setState({iconIndex: nextIconIndex}); + } + curIconMd5 () { + if (this.props.icons && + this.state.isRotatingIcon && + this.state.iconIndex < this.props.icons.length && + this.props.icons[this.state.iconIndex] && + this.props.icons[this.state.iconIndex].baseLayerMD5) { + return this.props.icons[this.state.iconIndex].baseLayerMD5; + } + return this.props.iconMd5; + } + render () { + const iconMd5 = this.curIconMd5(); + const iconURL = iconMd5 ? + `https://cdn.assets.scratch.mit.edu/internalapi/asset/${iconMd5}/get/` : + this.props.iconRawURL; + return ( + <LibraryItemComponent + bluetoothRequired={this.props.bluetoothRequired} + collaborator={this.props.collaborator} + description={this.props.description} + disabled={this.props.disabled} + extensionId={this.props.extensionId} + featured={this.props.featured} + hidden={this.props.hidden} + iconURL={iconURL} + icons={this.props.icons} + id={this.props.id} + insetIconURL={this.props.insetIconURL} + internetConnectionRequired={this.props.internetConnectionRequired} + name={this.props.name} + onBlur={this.handleBlur} + onFocus={this.handleFocus} + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} + onSelect={this.props.onSelect} + /> + ); + } +} + +LibraryItem.propTypes = { + bluetoothRequired: PropTypes.bool, + collaborator: PropTypes.string, + description: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.node + ]), + disabled: PropTypes.bool, + extensionId: PropTypes.string, + featured: PropTypes.bool, + hidden: PropTypes.bool, + iconMd5: PropTypes.string, + iconRawURL: PropTypes.string, + icons: PropTypes.arrayOf( + PropTypes.shape({ + baseLayerMD5: PropTypes.string + }) + ), + id: PropTypes.number.isRequired, + insetIconURL: PropTypes.string, + internetConnectionRequired: PropTypes.bool, + name: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.node + ]), + onMouseEnter: PropTypes.func.isRequired, + onMouseLeave: PropTypes.func.isRequired, + onSelect: PropTypes.func.isRequired +}; + +export default injectIntl(LibraryItem); diff --git a/src/containers/sprite-library.jsx b/src/containers/sprite-library.jsx index 72efa4fc9..b1e53386b 100644 --- a/src/containers/sprite-library.jsx +++ b/src/containers/sprite-library.jsx @@ -22,21 +22,8 @@ class SpriteLibrary extends React.PureComponent { constructor (props) { super(props); bindAll(this, [ - 'handleItemSelect', - 'handleMouseEnter', - 'handleMouseLeave', - 'rotateCostume', - 'startRotatingCostumes', - 'stopRotatingCostumes' + 'handleItemSelect' ]); - this.state = { - activeSprite: null, - costumeIndex: 0, - sprites: spriteLibraryContent - }; - } - componentWillUnmount () { - clearInterval(this.intervalId); } handleItemSelect (item) { this.props.vm.addSprite(JSON.stringify(item.json)).then(() => { @@ -48,46 +35,13 @@ class SpriteLibrary extends React.PureComponent { label: item.name }); } - handleMouseEnter (item) { - this.stopRotatingCostumes(); - this.setState({activeSprite: item}, this.startRotatingCostumes); - } - handleMouseLeave () { - this.stopRotatingCostumes(); - } - startRotatingCostumes () { - if (!this.state.activeSprite) return; - this.rotateCostume(); - this.intervalId = setInterval(this.rotateCostume, 300); - } - stopRotatingCostumes () { - this.intervalId = clearInterval(this.intervalId); - } - rotateCostume () { - const costumes = this.state.activeSprite.json.costumes; - const nextCostumeIndex = (this.state.costumeIndex + 1) % costumes.length; - this.setState({ - costumeIndex: nextCostumeIndex, - sprites: this.state.sprites.map(sprite => { - if (sprite.name === this.state.activeSprite.name) { - return { - ...sprite, - md5: sprite.json.costumes[nextCostumeIndex].baseLayerMD5 - }; - } - return sprite; - }) - }); - } render () { return ( <LibraryComponent - data={this.state.sprites} + data={spriteLibraryContent} id="spriteLibrary" tags={spriteTags} title={this.props.intl.formatMessage(messages.libraryTitle)} - onItemMouseEnter={this.handleMouseEnter} - onItemMouseLeave={this.handleMouseLeave} onItemSelected={this.handleItemSelect} onRequestClose={this.props.onRequestClose} /> diff --git a/test/integration/costumes.test.js b/test/integration/costumes.test.js index d46e1a4b0..5181c429a 100644 --- a/test/integration/costumes.test.js +++ b/test/integration/costumes.test.js @@ -164,4 +164,21 @@ describe('Working with costumes', () => { const logs = await getLogs(); await expect(logs).toEqual([]); }); + + test('Costumes animate on mouseover', async () => { + await loadUri(uri); + await clickXpath('//button[@title="Try It"]'); + await clickXpath('//button[@aria-label="Choose a Sprite"]'); + const searchElement = await findByXpath("//input[@placeholder='Search']"); + await searchElement.sendKeys('abb'); + const abbyElement = await findByXpath('//*[span[text()="Abby"]]'); + driver.actions() + .mouseMove(abbyElement) + .perform(); + // wait for one of Abby's alternate costumes to appear + await findByXpath('//img[@src="https://cdn.assets.scratch.mit.edu/internalapi/asset/b6e23922f23b49ddc6f62f675e77417c.svg/get/"]'); + const logs = await getLogs(); + await expect(logs).toEqual([]); + }); + }); -- GitLab