Skip to content

Revert List component to old API #62

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 50 additions & 16 deletions packages/peregrine/src/List/__tests__/items.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,32 @@ import { Items } from '..';

configure({ adapter: new Adapter() });

const items = Object.entries({
a: { id: 'a', val: '10' },
b: { id: 'b', val: '20' },
c: { id: 'c', val: '30' }
});
const items = [
{
id: '001',
name: 'Test Product 1',
small_image: '/test/product/1.png',
price: {
regularPrice: {
amount: {
value: 100
}
}
}
},
{
id: '002',
name: 'Test Product 2',
small_image: '/test/product/2.png',
price: {
regularPrice: {
amount: {
value: 100
}
}
}
}
];

test('renders a fragment', () => {
const props = { items };
Expand Down Expand Up @@ -59,7 +80,8 @@ test('passes correct props to each child', () => {
const wrapper = shallow(<Items {...props} />);

wrapper.children().forEach((node, i) => {
const [key, item] = items[i];
const item = items[i];
const key = item.id;

expect(node.key()).toEqual(key);
expect(node.props()).toMatchObject({
Expand All @@ -74,6 +96,17 @@ test('passes correct props to each child', () => {
});
});

test('uses keys generated by `getItemKey` if provided', () => {
const identity = x => x;
const tags = ['a', 'b', 'c'];
const props = { items: tags, getItemKey: identity };
const wrapper = shallow(<Items {...props} />);

wrapper.children().forEach((node, i) => {
expect(node.key()).toEqual(tags[i]);
});
});

test('indicates the child at index `cursor` has focus', () => {
const props = { items };
const wrapper = shallow(<Items {...props} />);
Expand All @@ -82,7 +115,7 @@ test('indicates the child at index `cursor` has focus', () => {
wrapper.setState(state);

wrapper.children().forEach((node, i) => {
const [, item] = items[i];
const item = items[i];

expect(node.props()).toMatchObject({
item,
Expand All @@ -100,7 +133,7 @@ test('indicates no child has focus if the list is not focused', () => {
wrapper.setState(state);

wrapper.children().forEach((node, i) => {
const [, item] = items[i];
const item = items[i];

expect(node.props()).toMatchObject({
item,
Expand All @@ -113,12 +146,13 @@ test('indicates no child has focus if the list is not focused', () => {
test('indicates whether a child is selected', () => {
const props = { items };
const wrapper = shallow(<Items {...props} />);
const selection = new Set().add('b').add('c');
const selection = new Set().add('002');

wrapper.setState({ selection });

wrapper.children().forEach((node, i) => {
const [key, item] = items[i];
const item = items[i];
const key = item.id;

expect(node.props()).toMatchObject({
item,
Expand Down Expand Up @@ -158,13 +192,13 @@ test('updates radio `selection` on child click', () => {
expect(wrapper.state('selection')).toEqual(new Set());

wrapper.childAt(0).simulate('click');
expect(wrapper.state('selection')).toEqual(new Set(['a']));
expect(wrapper.state('selection')).toEqual(new Set(['001']));

wrapper.childAt(1).simulate('click');
expect(wrapper.state('selection')).toEqual(new Set(['b']));
expect(wrapper.state('selection')).toEqual(new Set(['002']));

wrapper.childAt(0).simulate('click');
expect(wrapper.state('selection')).toEqual(new Set(['a']));
expect(wrapper.state('selection')).toEqual(new Set(['001']));
});

test('updates checkbox `selection` on child click', () => {
Expand All @@ -174,13 +208,13 @@ test('updates checkbox `selection` on child click', () => {
expect(wrapper.state('selection')).toEqual(new Set());

wrapper.childAt(0).simulate('click');
expect(wrapper.state('selection')).toEqual(new Set(['a']));
expect(wrapper.state('selection')).toEqual(new Set(['001']));

wrapper.childAt(1).simulate('click');
expect(wrapper.state('selection')).toEqual(new Set(['a', 'b']));
expect(wrapper.state('selection')).toEqual(new Set(['001', '002']));

wrapper.childAt(0).simulate('click');
expect(wrapper.state('selection')).toEqual(new Set(['b']));
expect(wrapper.state('selection')).toEqual(new Set(['002']));
});

test('calls `syncSelection` after updating selection', () => {
Expand Down
30 changes: 26 additions & 4 deletions packages/peregrine/src/List/__tests__/list.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,32 @@ const classes = {
root: 'abc'
};

const items = new Map()
.set('a', { id: 'a' })
.set('b', { id: 'b' })
.set('c', { id: 'c' });
const items = [
{
id: '001',
name: 'Test Product 1',
small_image: '/test/product/1.png',
price: {
regularPrice: {
amount: {
value: 100
}
}
}
},
{
id: '002',
name: 'Test Product 2',
small_image: '/test/product/2.png',
price: {
regularPrice: {
amount: {
value: 100
}
}
}
}
];

test('renders a div by default', () => {
const props = { classes };
Expand Down
38 changes: 21 additions & 17 deletions packages/peregrine/src/List/items.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Component, Fragment, createElement } from 'react';
import PropTypes from 'prop-types';

import memoize from '../util/unaryMemoize';
import iterable from '../validators/iterable';
import ListItem from './item';

const removeFocus = () => ({
Expand Down Expand Up @@ -36,15 +37,14 @@ const updateSelection = memoize(key => (prevState, props) => {

class Items extends Component {
static propTypes = {
items: PropTypes.oneOfType([
PropTypes.instanceOf(Map),
PropTypes.arrayOf(PropTypes.array)
]).isRequired,
getItemKey: PropTypes.func.isRequired,
items: iterable.isRequired,
renderItem: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
selectionModel: PropTypes.oneOf(['checkbox', 'radio'])
};

static defaultProps = {
getItemKey: ({ id }) => id,
selectionModel: 'radio'
};

Expand All @@ -55,21 +55,25 @@ class Items extends Component {
};

render() {
const { items, renderItem } = this.props;
const { getItemKey, items, renderItem } = this.props;
const { cursor, hasFocus, selection } = this.state;

const children = Array.from(items, ([key, item], index) => (
<ListItem
key={key}
item={item}
render={renderItem}
hasFocus={hasFocus && cursor === index}
isSelected={selection.has(key)}
onBlur={this.handleBlur}
onClick={this.getClickHandler(key)}
onFocus={this.getFocusHandler(index)}
/>
));
const children = Array.from(items, (item, index) => {
const key = getItemKey(item, index);

return (
<ListItem
key={key}
item={item}
render={renderItem}
hasFocus={hasFocus && cursor === index}
isSelected={selection.has(key)}
onBlur={this.handleBlur}
onClick={this.getClickHandler(key)}
onFocus={this.getFocusHandler(index)}
/>
);
});

return <Fragment>{children}</Fragment>;
}
Expand Down
13 changes: 8 additions & 5 deletions packages/peregrine/src/List/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@ import { Component, createElement } from 'react';
import PropTypes from 'prop-types';

import fromRenderProp from '../util/fromRenderProp';
import iterable from '../validators/iterable';
import Items from './items';

class List extends Component {
static propTypes = {
classes: PropTypes.shape({
root: PropTypes.string
}),
items: PropTypes.oneOfType([
PropTypes.instanceOf(Map),
PropTypes.arrayOf(PropTypes.array)
]).isRequired,
getItemKey: PropTypes.func.isRequired,
items: iterable.isRequired,
render: PropTypes.oneOfType([PropTypes.func, PropTypes.string])
.isRequired,
renderItem: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
Expand All @@ -22,7 +21,8 @@ class List extends Component {

static defaultProps = {
classes: {},
items: new Map(),
getItemKey: ({ id }) => id,
items: [],
render: 'div',
renderItem: 'div',
selectionModel: 'radio'
Expand All @@ -31,6 +31,7 @@ class List extends Component {
render() {
const {
classes,
getItemKey,
items,
render,
renderItem,
Expand All @@ -41,6 +42,7 @@ class List extends Component {

const customProps = {
classes,
getItemKey,
items,
onSelectionChange,
selectionModel
Expand All @@ -52,6 +54,7 @@ class List extends Component {
<Root className={classes.root} {...customProps} {...restProps}>
<Items
items={items}
getItemKey={getItemKey}
renderItem={renderItem}
selectionModel={selectionModel}
onSelectionChange={this.handleSelectionChange}
Expand Down
70 changes: 70 additions & 0 deletions packages/peregrine/src/validators/__tests__/iterable.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import iterable from '../iterable';

const name = 'MyComponent';

const props = {
b: null,
c: '',
d: [],
e: new Map(),
f: {}
};

test('returns nothing if prop is undefined', () => {
const result = iterable(props, 'a', name);

expect(result).toBeUndefined();
});

test('returns nothing if prop is `null`', () => {
const result = iterable(props, 'b', name);

expect(result).toBeUndefined();
});

test('returns an error if required and prop is undefined', () => {
const result = iterable.isRequired(props, 'a', name);

expect(result).toBeInstanceOf(Error);
});

test('returns an error if required and prop is `null`', () => {
const result = iterable.isRequired(props, 'b', name);

expect(result).toBeInstanceOf(Error);
});

test('returns nothing if prop is a string', () => {
const result = iterable(props, 'c', name);

expect(result).toBeUndefined();
});

test('returns nothing if prop is an array', () => {
const result = iterable(props, 'd', name);

expect(result).toBeUndefined();
});

test('returns nothing if prop is a Map', () => {
const result = iterable(props, 'e', name);

expect(result).toBeUndefined();
});

test('returns an error if prop is not iterable', () => {
const result = iterable(props, 'f', name);

expect(result).toBeInstanceOf(Error);
});

test('returns a proper error object', () => {
const result = iterable(props, 'f', name);
const thrower = () => {
throw result;
};

expect(thrower).toThrow(
'Invalid prop `f` of type `object` supplied to `MyComponent`, expected `iterable`.'
);
});
27 changes: 27 additions & 0 deletions packages/peregrine/src/validators/iterable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const isIterable = obj => typeof obj[Symbol.iterator] === 'function';

function optionalIterable(props, propName, componentName) {
const prop = props[propName];
const type = typeof prop;

if (prop != null && !isIterable(prop)) {
return new Error(
`Invalid prop \`${propName}\` of type \`${type}\` supplied to \`${componentName}\`, expected \`iterable\`.`
);
}
}

function requiredIterable(props, propName, componentName) {
const prop = props[propName];
const type = typeof prop;

if (prop == null || !isIterable(prop)) {
return new Error(
`Invalid prop \`${propName}\` of type \`${type}\` supplied to \`${componentName}\`, expected \`iterable\`.`
);
}
}

optionalIterable.isRequired = requiredIterable;

export default optionalIterable;
Loading