Skip to content

Commit

Permalink
feat(chips): Add selection to chips (#121)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Users should render `Chip` components directly instead of passing a `labels` prop to `ChipSet`.
  • Loading branch information
bonniezhou authored Jul 18, 2018
1 parent 3797a06 commit 3ef1123
Show file tree
Hide file tree
Showing 12 changed files with 389 additions and 150 deletions.
102 changes: 102 additions & 0 deletions packages/chips/Chip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, {Component} from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import withRipple from '../ripple';
import {MDCChipFoundation} from '@material/chips';

export class Chip extends Component {
foundation_ = null;
state = {
classList: new Set(),
};

componentDidMount() {
this.foundation_ = new MDCChipFoundation(this.adapter);
this.foundation_.init();
}

componentWillUnmount() {
this.foundation_.destroy();
}

get classes() {
const {classList} = this.state;
const {className, selected} = this.props;
return classnames('mdc-chip', Array.from(classList), className, {
'mdc-chip--selected': selected,
});
}

get adapter() {
return {
addClass: (className) => {
const classList = new Set(this.state.classList);
classList.add(className);
this.setState({classList});
},
removeClass: (className) => {
const classList = new Set(this.state.classList);
classList.delete(className);
this.setState({classList});
},
hasClass: (className) => this.classes.split(' ').includes(className),
};
}

handleClick = (e) => {
if (typeof this.props.onClick === 'function') {
this.props.onClick(e);
}
this.props.handleSelect(this.props.id);
}

render() {
const {
className, // eslint-disable-line no-unused-vars
label,
handleSelect, // eslint-disable-line no-unused-vars
onClick, // eslint-disable-line no-unused-vars
chipCheckmark,
computeBoundingRect, // eslint-disable-line no-unused-vars
initRipple,
unbounded, // eslint-disable-line no-unused-vars
...otherProps
} = this.props;

return (
<div
className={this.classes}
onClick={this.handleClick}
ref={initRipple}
{...otherProps}
>
{chipCheckmark}
<div className='mdc-chip__text'>{label}</div>
</div>
);
}
}

Chip.propTypes = {
id: PropTypes.number,
label: PropTypes.string,
className: PropTypes.string,
selected: PropTypes.bool,
handleSelect: PropTypes.func,
onClick: PropTypes.func,
// The following props are handled by withRipple and do not require defaults.
initRipple: PropTypes.func,
unbounded: PropTypes.bool,
chipCheckmark: PropTypes.node,
computeBoundingRect: PropTypes.func,
};

Chip.defaultProps = {
id: -1,
label: '',
className: '',
selected: false,
handleSelect: () => {},
};

export default withRipple(Chip);
29 changes: 29 additions & 0 deletions packages/chips/ChipCheckmark.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, {Component} from 'react';

export default class ChipCheckmark extends Component {
width = null;

init = (element) => {
if (!element) {
return;
}

// The checkmark's width may initially be set to 0, so use the checkmark's height as a proxy since the
// checkmark should always be square.
this.width = element.getBoundingClientRect().height;
}

render() {
return (
<div className='mdc-chip__checkmark' ref={this.init}>
<svg className='mdc-chip__checkmark-svg' viewBox='-2 -3 30 30'>
<path
className='mdc-chip__checkmark-path'
fill='none'
stroke='black'
d='M1.73,12.91 8.1,19.28 22.79,4.59'/>
</svg>
</div>
);
}
}
63 changes: 63 additions & 0 deletions packages/chips/ChipSet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, {Component} from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';

import ChipCheckmark from './ChipCheckmark';
import Chip from './Chip';

export default class ChipSet extends Component {
checkmarkWidth_ = 0;

get classes() {
const {className} = this.props;
return classnames('mdc-chip-set', className);
}

get adapter() {
return {
hasClass: (className) => this.classes.split(' ').includes(className),
};
}

setCheckmarkWidth = (checkmark) => {
if (!!this.checkmarkWidth_) {
return;
}
this.checkmarkWidth_ = checkmark.width;
}

computeBoundingRect = (chipElement) => {
const height = chipElement.getBoundingClientRect().height;
const width = chipElement.getBoundingClientRect().width + this.checkmarkWidth_;
return {height, width};
}

renderChip = (chip) => {
return (
<Chip
chipCheckmark={this.props.filter ? <ChipCheckmark ref={this.setCheckmarkWidth}/> : null}
computeBoundingRect={this.props.filter ? this.computeBoundingRect : null}
{...chip.props} />
);
}

render() {
return (
<div className={this.classes}>
{React.Children.map(this.props.children, this.renderChip)}
</div>
);
}
}

ChipSet.propTypes = {
className: PropTypes.string,
filter: PropTypes.bool,
children: PropTypes.node,
};

ChipSet.defaultProps = {
className: '',
filter: false,
children: null,
};
92 changes: 87 additions & 5 deletions packages/chips/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ A React version of an [MDC Chips](https://github.com/material-components/materia
## Installation

```
npm install --save @material/react-chips
npm install @material/react-chips
```

## Usage
Expand All @@ -31,7 +31,84 @@ import ChipSet from '@material/react-chips';
class MyApp extends Component {
render() {
return (
<ChipSet labels={['Chip One', 'Chip Two']} />
<ChipSet>
<Chip id={0} label='Summer'/>
<Chip id={1} label='Winter'/>
</ChipSet>
);
}
}
```

## Variants

### Selection

There are two types of chips that allow for selection: [choice chips](https://material.io/design/components/chips.html#choice-chips) for single selection, and [filter chips](https://material.io/design/components/chips.html#filter-chips) for multiple selection. You can indicate a `Chip` is selected by adding the `selected` prop. Due to React's uni-directional data flow, you are expected write your own selection logic and pass a callback to the `Chip` through the `handleSelect` prop.

#### Choice chips

```js
class MyChoiceChips extends React.Component {
state = {
selectedChipId: -1,
};

isSelected = (id) => {
return this.state.selectedChipId === id;
}

handleSelect = (id) => {
if (this.isSelected(id)) {
this.setState({selectedChipId: -1});
} else {
this.setState({selectedChipId: id});
}
}

render() {
return (
<ChipSet>
<Chip selected={this.isSelected(0)} id={0} label='Small' handleSelect={this.handleSelect}/>
<Chip selected={this.isSelected(1)} id={1} label='Medium' handleSelect={this.handleSelect}/>
<Chip selected={this.isSelected(2)} id={2} label='Large' handleSelect={this.handleSelect}/>
</ChipSet>
);
}
}
```

#### Filter chips

Filter chips include a leading checkmark to indicate selection. To define a set of chips as filter chips, add the `filter` prop to the `ChipSet`.

```js
class MyFilterChips extends React.Component {
state = {
selectedChipIds: new Set(),
};

isSelected = (id) => {
return this.state.selectedChipIds.has(id);
}

handleSelect = (id) => {
const selectedChipIds = new Set(this.state.selectedChipIds);
if (this.isSelected(id)) {
selectedChipIds.delete(id);
} else {
selectedChipIds.add(id);
}
this.setState({selectedChipIds});
}

render() {
return (
<ChipSet filter>
<Chip selected={this.isSelected(0)} id={0} label='Tops' handleSelect={this.handleSelect}/>
<Chip selected={this.isSelected(1)} id={1} label='Bottoms' handleSelect={this.handleSelect}/>
<Chip selected={this.isSelected(2)} id={2} label='Shoes' handleSelect={this.handleSelect}/>
</ChipSet>
);
}
}
Expand All @@ -43,14 +120,19 @@ class MyApp extends Component {

Prop Name | Type | Description
--- | --- | ---
className | String | Classes to be applied to the chip set element.
labels | Array | An array of strings. Each string has a corresponding chip whose label will be set to the value of that string.
className | String | Classes to be applied to the chip set element
filter | Boolean | Indicates that the chips in the set are filter chips, which allow multiple selection from a set of options


### Chip

Prop Name | Type | Description
--- | --- | ---
className | String | Classes to be applied to the chip element.
className | String | Classes to be applied to the chip element
id | Number | Unique identifier for the chip
label | String | Text to be shown on the chip
selected | Boolean | Indicates whether the chip is selected
handleSelect | Function(id: number) => void | Callback to call when the chip with the given id is selected

## Sass Mixins

Expand Down
51 changes: 0 additions & 51 deletions packages/chips/chip-set/index.js

This file was deleted.

Loading

0 comments on commit 3ef1123

Please sign in to comment.