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 ? (
{/* Layers of wrapping is to prevent layout thrashing on animation */}
@@ -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 (
-
- );
- })}
+ {this.getFilteredData().map((dataItem, index) => (
+
+ ))}
);
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 (
+
+ );
+ }
+}
+
+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 (
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([]);
+ });
+
});