added library-item container, moved costume rotation to library items
This commit is contained in:
1 parent
912306e671
commit
893beb5a15
5 files changed
+187
-111
No files matched your search
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
|
||||
});
|
||||
Reference in new issue
Block a user