diff --git a/src/components/library-item/library-item.css b/src/components/library-item/library-item.css
index 55edcc2e3..692e1b612 100644
--- a/src/components/library-item/library-item.css
+++ b/src/components/library-item/library-item.css
@@ -8,6 +8,7 @@
justify-content: flex-start;
flex-basis: 160px;
height: 160px;
+ max-width: 160px;
margin: $space;
padding: 1rem 1rem 0 1rem;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
diff --git a/src/components/library-item/library-item.jsx b/src/components/library-item/library-item.jsx
index 03a9d6b78..3880a4cf6 100644
--- a/src/components/library-item/library-item.jsx
+++ b/src/components/library-item/library-item.jsx
@@ -1,4 +1,3 @@
-import bindAll from 'lodash.bindall';
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
@@ -10,42 +9,8 @@ import classNames from 'classnames';
import bluetoothIconURL from './bluetooth.svg';
import internetConnectionIconURL from './internet-connection.svg';
-class LibraryItem extends React.PureComponent {
- constructor (props) {
- super(props);
- bindAll(this, [
- 'handleBlur',
- 'handleClick',
- 'handleFocus',
- 'handleKeyPress',
- 'handleMouseEnter',
- 'handleMouseLeave'
- ]);
- }
- 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);
- }
- e.preventDefault();
- }
- handleKeyPress (e) {
- if (e.key === ' ' || e.key === 'Enter') {
- e.preventDefault();
- this.props.onSelect(this.props.id);
- }
- }
- handleMouseEnter () {
- this.props.onMouseEnter(this.props.id);
- }
- handleMouseLeave () {
- this.props.onMouseLeave(this.props.id);
- }
+/* eslint-disable react/prefer-stateless-function */
+class LibraryItemComponent extends React.PureComponent {
render () {
return this.props.featured ? (
{this.props.disabled ? (
@@ -146,12 +111,12 @@ class LibraryItem extends React.PureComponent {
)}
role="button"
tabIndex="0"
- onBlur={this.handleBlur}
- onClick={this.handleClick}
- onFocus={this.handleFocus}
- onKeyPress={this.handleKeyPress}
- onMouseEnter={this.handleMouseEnter}
- onMouseLeave={this.handleMouseLeave}
+ onBlur={this.props.onBlur}
+ onClick={this.props.onClick}
+ onFocus={this.props.onFocus}
+ onKeyPress={this.props.onKeyPress}
+ onMouseEnter={this.props.onMouseEnter}
+ onMouseLeave={this.props.onMouseLeave}
>
{/* Layers of wrapping is to prevent layout thrashing on animation */}
@@ -167,8 +132,10 @@ class LibraryItem extends React.PureComponent {
);
}
}
+/* eslint-enable react/prefer-stateless-function */
-LibraryItem.propTypes = {
+
+LibraryItemComponent.propTypes = {
bluetoothRequired: PropTypes.bool,
collaborator: PropTypes.string,
description: PropTypes.oneOfType([
@@ -180,22 +147,22 @@ LibraryItem.propTypes = {
featured: PropTypes.bool,
hidden: PropTypes.bool,
iconURL: PropTypes.string,
- id: PropTypes.number.isRequired,
insetIconURL: PropTypes.string,
internetConnectionRequired: PropTypes.bool,
name: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node
]),
- onBlur: PropTypes.func,
- onFocus: PropTypes.func,
+ onBlur: PropTypes.func.isRequired,
+ onClick: PropTypes.func.isRequired,
+ onFocus: PropTypes.func.isRequired,
+ onKeyPress: PropTypes.func.isRequired,
onMouseEnter: PropTypes.func.isRequired,
- onMouseLeave: PropTypes.func.isRequired,
- onSelect: PropTypes.func.isRequired
+ onMouseLeave: 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..8073b0f49 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]);
@@ -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..6177b916f
--- /dev/null
+++ b/src/containers/library-item.jsx
@@ -0,0 +1,149 @@
+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',
+ 'handleClick',
+ 'handleFocus',
+ 'handleKeyPress',
+ 'handleMouseEnter',
+ 'handleMouseLeave',
+ 'rotateIcon',
+ 'startRotatingIcons',
+ 'stopRotatingIcons'
+ ]);
+ this.state = {
+ iconIndex: 0,
+ isRotatingIcon: false
+ };
+ }
+ componentWillUnmount () {
+ clearInterval(this.intervalId);
+ }
+ handleBlur (id) {
+ this.handleMouseLeave(id);
+ }
+ handleClick (e) {
+ if (!this.props.disabled) {
+ this.props.onSelect(this.props.id);
+ }
+ e.preventDefault();
+ }
+ handleFocus (id) {
+ this.handleMouseEnter(id);
+ }
+ handleKeyPress (e) {
+ if (e.key === ' ' || e.key === 'Enter') {
+ e.preventDefault();
+ this.props.onSelect(this.props.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 dd4c3dddb..7f885cbe4 100644
--- a/src/containers/sprite-library.jsx
+++ b/src/containers/sprite-library.jsx
@@ -23,21 +23,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) {
// Randomize position of library sprite
@@ -51,46 +38,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([]);
+ });
+
});