Skip to content

Commit

Permalink
Add list components (#32)
Browse files Browse the repository at this point in the history
* Add dependency: lodash.memoize

Also, add a script for running jest in watch mode.

* Add list components

- Bring `List` and its dependencies from the theme repo
- Add tests for `List`, `Items`, and `Item`
- Improve them based on issues encountered while testing

* Re-export List components

* Add storybooks for List

* Fix broken test after jest upgrade

* Fix tests and docs

* Update list docs

* Remove lodash dependency

Replace it with a small memoize utility for unary functions.

* Add @storybook/addons
  • Loading branch information
jimbo authored and Andrew Levine committed Jun 13, 2018
1 parent 10a6d62 commit 7a8ea9a
Show file tree
Hide file tree
Showing 21 changed files with 3,645 additions and 1,501 deletions.
4,212 changes: 2,722 additions & 1,490 deletions packages/peregrine/package-lock.json

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions packages/peregrine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
"danger": "danger-ci",
"clean": "rimraf dist",
"lint": "eslint 'src/**/*.js'",
"prepare": "npm-merge-driver install",
"prettier": "prettier --write '{src/**/,scripts/**/,}*.js'",
"prettier:check": "prettier --list-different '{src/**/,scripts/**/,}*.js'",
"test": "jest --no-cache --coverage",
"test:debug": "node --inspect-brk node_modules/.bin/jest -i",
"test:dev": "jest --watch",
"storybook": "start-storybook -p 9001 -c .storybook",
"storybook:build": "build-storybook -c .storybook -o storybook-dist"
},
Expand All @@ -36,6 +38,7 @@
"devDependencies": {
"@magento/eslint-config": "^1.0.0",
"@storybook/addon-actions": "^3.4.2",
"@storybook/addons": "^3.4.6",
"@storybook/react": "^3.4.2",
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
Expand All @@ -47,14 +50,15 @@
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.1",
"danger": "^3.4.5",
"enzyme": "^3.2.0",
"enzyme-adapter-react-16": "^1.1.0",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"eslint": "^4.12.1",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-react": "^7.5.1",
"execa": "^0.10.0",
"jest": "^22.4.2",
"jest": "^22.4.3",
"jest-fetch-mock": "^1.4.1",
"npm-merge-driver": "^2.3.5",
"prettier": "^1.8.2",
"prop-types": "^15.6.0",
"react": "^16.2.0",
Expand Down
17 changes: 17 additions & 0 deletions packages/peregrine/src/List/__docs__/item.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Item

The `Item` component is a direct child of the `Items` fragment.

## Usage

See `List`.

## Props

Prop Name | Required? | Description
--------- | :-------: | :----------
`classes` | ❌ | A classname object.
`hasFocus` | ❌ | Whether the element currently has browser focus
`isSelected` | ❌ | Whether the item is currently selected
`item` | ✅ | A data object. If `item` is a string, it will be rendered as a child
`render` | ✅ | A [render prop](https://reactjs.org/docs/render-props.html). Also accepts a tagname (e.g., `"div"`)
15 changes: 15 additions & 0 deletions packages/peregrine/src/List/__docs__/items.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Items

The `Items` component is a direct child of the `List` component. As a fragment, it returns its children directly, with no wrapping element.

## Usage

See `List`.

## Props

Prop Name | Required? | Description
--------- | :-------: | :----------
`items` | ✅ | An iterable that yields `[key, item]` pairs, such as an ES2015 `Map`
`renderItem` | ❌ | A [render prop](https://reactjs.org/docs/render-props.html). Also accepts a tagname (e.g., `"div"`)
`selectionModel` | ❌ | A string specifying whether to use a `radio` or `checkbox` selection model
44 changes: 44 additions & 0 deletions packages/peregrine/src/List/__docs__/list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# List

The `List` component maps a collection of data objects into an array of elements. It also manages the selection and focus of those elements.

## Usage

```jsx
import { List } from '@magento/peregrine';

const simpleData = new Map()
.set('s', 'Small')
.set('m', 'Medium')
.set('l', 'Large')

<List
classes={{ root: 'foo' }}
items={simpleData}
render={'ul'}
renderItem={'li'}
/>

const complexData = new Map()
.set('s', { id: 's', value: 'Small' })
.set('m', { id: 'm', value: 'Medium' })
.set('l', { id: 'l', value: 'Large' })

<List
classes={{ root: 'bar' }}
items={complexData}
render={props => (<ul>{props.children}</ul>)}
renderItem={props => (<li>{props.item.value}</li>)}
/>
```

## Props

Prop Name | Required? | Description
--------- | :-------: | :----------
`classes` | ❌ | A classname hash
`items` | ✅ | An iterable that yields `[key, item]` pairs, such as an ES2015 `Map`
`render` | ✅ | A [render prop](https://reactjs.org/docs/render-props.html) for the list element. Also accepts a tagname (e.g., `"div"`)
`renderItem` | ❌ | A [render prop](https://reactjs.org/docs/render-props.html) for the list item elements. Also accepts a tagname (e.g., `"div"`)
`onSelectionChange` | ❌ | A callback fired when the selection state changes
`selectionModel` | ❌ | A string specifying whether to use a `radio` or `checkbox` selection model
15 changes: 15 additions & 0 deletions packages/peregrine/src/List/__stories__/item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createElement } from 'react';
import { storiesOf } from '@storybook/react';
import { withReadme } from 'storybook-readme';

import Item from '..';
import docs from '../__docs__/item.md';

const stories = storiesOf('Item', module);

stories.add(
'default',
withReadme(docs, () => (
<Item classes={{ root: 'foo' }} item={{ id: 's', value: 'Small' }} />
))
);
19 changes: 19 additions & 0 deletions packages/peregrine/src/List/__stories__/items.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createElement } from 'react';
import { storiesOf } from '@storybook/react';
import { withReadme } from 'storybook-readme';

import Items from '..';
import docs from '../__docs__/items.md';

const data = {
s: { id: 's', value: 'Small' },
m: { id: 'm', value: 'Medium' },
l: { id: 'l', value: 'Large' }
};

const stories = storiesOf('Items', module);

stories.add(
'default',
withReadme(docs, () => <Items items={Object.entries(data)} />)
);
44 changes: 44 additions & 0 deletions packages/peregrine/src/List/__stories__/list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { createElement } from 'react';
import { storiesOf } from '@storybook/react';
import { withReadme } from 'storybook-readme';

import List from '..';
import docs from '../__docs__/list.md';

const stories = storiesOf('List', module);

// simple example with string values
const simpleData = new Map()
.set('s', 'Small')
.set('m', 'Medium')
.set('l', 'Large');

stories.add(
'simple',
withReadme(docs, () => (
<List
classes={{ root: 'foo' }}
items={simpleData}
render={'ul'}
renderItem={'li'}
/>
))
);

// complex example with object values
const complexData = new Map()
.set('s', { id: 's', value: 'Small' })
.set('m', { id: 'm', value: 'Medium' })
.set('l', { id: 'l', value: 'Large' });

stories.add(
'complex',
withReadme(docs, () => (
<List
classes={{ root: 'bar' }}
items={complexData}
render={props => <ul>{props.children}</ul>}
renderItem={props => <li>{props.item.value}</li>}
/>
))
);
73 changes: 73 additions & 0 deletions packages/peregrine/src/List/__tests__/item.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { createElement } from 'react';
import { configure, shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

import { Item } from '../index.js';

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

const classes = {
root: 'abc'
};

test('renders a div by default', () => {
const props = { item: 'a' };
const wrapper = shallow(<Item {...props} />).dive();

expect(wrapper.type()).toEqual('div');
});

test('renders a provided tagname', () => {
const props = { item: 'a', render: 'p' };
const wrapper = shallow(<Item {...props} />).dive();

expect(wrapper.type()).toEqual('p');
});

test('renders a provided component', () => {
const Span = () => <span />;
const props = { item: 'a', render: Span };
const wrapper = shallow(<Item {...props} />);

expect(wrapper.type()).toEqual(Span);
expect(wrapper.dive().type()).toEqual('span');
});

test('passes only rest props to basic `render`', () => {
const props = { classes, item: 'a', render: 'p' };
const wrapper = shallow(<Item {...props} data-id="b" />).dive();

expect(wrapper.props()).toHaveProperty('data-id');
expect(wrapper.props()).not.toHaveProperty('classes');
expect(wrapper.props()).not.toHaveProperty('hasFocus');
expect(wrapper.props()).not.toHaveProperty('isSelected');
expect(wrapper.props()).not.toHaveProperty('item');
expect(wrapper.props()).not.toHaveProperty('render');
});

test('passes custom and rest props to composite `render`', () => {
const Span = () => <span />;
const props = { classes, item: 'a', render: Span };
const wrapper = shallow(<Item {...props} data-id="b" />);

expect(wrapper.props()).toHaveProperty('data-id');
expect(wrapper.props()).toHaveProperty('classes');
expect(wrapper.props()).toHaveProperty('hasFocus');
expect(wrapper.props()).toHaveProperty('isSelected');
expect(wrapper.props()).toHaveProperty('item');
expect(wrapper.props()).not.toHaveProperty('render');
});

test('passes `item` as `children` if `item` is a string', () => {
const props = { item: 'a', render: 'p' };
const wrapper = shallow(<Item {...props} />).dive();

expect(wrapper.text()).toEqual('a');
});

test('does not pass `children` if `item` is not a string', () => {
const props = { item: { id: 1 }, render: 'p' };
const wrapper = shallow(<Item {...props} />).dive();

expect(wrapper.text()).toBe('');
});
Loading

0 comments on commit 7a8ea9a

Please sign in to comment.