Skip to content

Commit

Permalink
feat: adds support for dropdown being used as controlled component
Browse files Browse the repository at this point in the history
  • Loading branch information
Gabriel Micko committed Mar 1, 2019
1 parent c1a27b7 commit 5928d80
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 45 deletions.
96 changes: 58 additions & 38 deletions packages/react-dropdown/src/MultiDownshift.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export type MultiControllerStateAndHelpers = ControllerStateAndHelpers<DropdownS
};

type Props = {
selectedItems: Array<DropdownSelectedItem>,
initialSelectedItems: Array<DropdownSelectedItem>,
selectedItems?: Array<DropdownSelectedItem>,
onSelect?: (
Array<DropdownSelectedItem>,
MultiControllerStateAndHelpers
Expand All @@ -38,7 +39,9 @@ type State = {
};

class MultiDownshift extends React.Component<Props, State> {
state = { selectedItems: this.props.selectedItems };
state = {
selectedItems: this.props.initialSelectedItems,
};

stateReducer = (
state: ControllerStateAndHelpers<DropdownSelectedItem>,
Expand All @@ -65,80 +68,97 @@ class MultiDownshift extends React.Component<Props, State> {
selectedItem: DropdownSelectedItem | null,
downshift: ControllerStateAndHelpers<DropdownSelectedItem>
) => {
const callOnChange = () => {
const callOnChange = selectedItems => {
const { onChange } = this.props;
const { selectedItems } = this.state;
if (onChange) {
onChange(selectedItems, this.getStateAndHelpers(downshift));
}
};

const selectedItems = this.getSelectedItems();
let newSelectedItems = [];
if (selectedItem === null) {
this.clearItems(callOnChange);
newSelectedItems = this.clearItems();
} else {
if (this.props.multiselect) {
if (includesId(this.state.selectedItems, selectedItem.id)) {
this.removeItem(selectedItem, callOnChange);
if (includesId(selectedItems, selectedItem.id)) {
newSelectedItems = this.removeItem(selectedItem, selectedItems);
} else {
this.addSelectedItem(selectedItem, callOnChange);
newSelectedItems = this.addSelectedItem(selectedItem, selectedItems);
}
} else {
this.replaceItem(selectedItem, callOnChange);
newSelectedItems = this.replaceItem(selectedItem);
}
}

if (this.isSelectedItemsPresentInProps()) {
callOnChange(newSelectedItems);
} else {
this.setState(
{
selectedItems: newSelectedItems,
},
() => {
callOnChange(this.state.selectedItems);
}
);
}
};

clearItems = (cb?: () => void) => {
this.setState(({ selectedItems }) => {
return {
selectedItems: [],
};
}, cb);
isSelectedItemsPresentInProps() {
return this.props.selectedItems ? true : false;
}

getSelectedItems() {
if (this.props.selectedItems) {
return this.props.selectedItems;
}
return this.state.selectedItems;
}

clearItems = () => {
return [];
};

replaceItem = (item: DropdownSelectedItem, cb?: () => void) => {
this.setState(({ selectedItems }) => {
return {
selectedItems: [item],
};
}, cb);
replaceItem = (item: DropdownSelectedItem) => {
return [item];
};

removeItem = (item: DropdownSelectedItem, cb?: () => void) => {
this.setState(({ selectedItems }) => {
return {
selectedItems: selectedItems.filter(({ id }) => id !== item.id),
};
}, cb);
removeItem = (
item: DropdownSelectedItem,
selectedItems: Array<DropdownSelectedItem>
): Array<DropdownSelectedItem> => {
return selectedItems.filter(({ id }) => id !== item.id);
};

addSelectedItem = (item: DropdownSelectedItem, cb?: () => void) => {
this.setState(
({ selectedItems }) => ({
selectedItems: [...selectedItems, item],
}),
cb
);
addSelectedItem = (
item: DropdownSelectedItem,
selectedItems: Array<DropdownSelectedItem>
) => {
return [...selectedItems, item];
};

getStateAndHelpers = (
downshift: ControllerStateAndHelpers<DropdownSelectedItem>
): MultiControllerStateAndHelpers => {
const { selectedItems } = this.state;
const { removeItem } = this;

return {
removeItem,
selectedItems,
selectedItems: this.getSelectedItems(),
...downshift,
};
};

render() {
const { multiselect, children, selectedItem, ...props } = this.props;
const { multiselect, children, ...props } = this.props;
const selectedItems = this.getSelectedItems();
return (
<Downshift
{...props}
selectedItem={multiselect ? null : selectedItem}
selectedItem={
!multiselect && selectedItems.length ? selectedItems[0] : null
}
stateReducer={this.stateReducer}
onChange={this.handleSelection}
>
Expand Down
2 changes: 1 addition & 1 deletion packages/react-dropdown/src/MultiSelect.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ const handleDelete = ({ id }) => {
items={items}
categories={categories}
multiselect={true}
selectedItems={selectedItems}
initialSelectedItems={selectedItems}
onChange={mirrorDropdownState}
>
{({ getInputProps, removeItem, ...props }) => {
Expand Down
6 changes: 4 additions & 2 deletions packages/react-dropdown/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const DevFragment =
type Props = {
items: Array<DropdownItem>,
categories?: Array<DropdownCategory>,
initialSelectedItems?: Array<DropdownSelectedItem>,
selectedItems?: Array<DropdownSelectedItem>,
useFilter?: boolean,
filterFn?: (
Expand Down Expand Up @@ -74,7 +75,8 @@ const Dropdown = ({
children,
name = 'dropdown',
twoColumn = true,
selectedItems = [],
initialSelectedItems = [],
selectedItems,
initialIsOpen = false,
placement = 'bottom-start',
popperModifiers,
Expand All @@ -86,9 +88,9 @@ const Dropdown = ({
}: Props) => (
<Manager>
<MultiDownshift
initialSelectedItem={selectedItems.length ? selectedItems[0] : null}
multiselect={multiselect}
itemToString={item => (item && item.label ? item.label : '')}
initialSelectedItems={initialSelectedItems}
selectedItems={selectedItems}
initialIsOpen={initialIsOpen}
onChange={onChange}
Expand Down
40 changes: 38 additions & 2 deletions packages/react-dropdown/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,43 @@ const items = [
{ id: 131, label: 'One' },
];

<Dropdown items={items} selectedItems={[items[0]]}>
<Dropdown items={items} initialSelectedItems={[items[3]]}>
{({ getInputProps }) => <InputText readOnly {...getInputProps()} />}
</Dropdown>;
```

This is usecase will allow you to use `Dropdown` as a controlled component.

```js
initialState = {
selectedItems: [{ id: 10, label: 'One' }],
};

const items = [
{ id: 10, label: 'One' },
{ id: 22, label: 'Two' },
{ id: 33, label: 'Three' },
{ id: 44, label: 'Four' },
{ id: 55, label: 'Four' },
{ id: 66, label: 'Three' },
{ id: 77, label: 'Four' },
{ id: 88, label: 'Three' },
{ id: 99, label: 'Four' },
{ id: 101, label: 'Three' },
{ id: 111, label: 'Four' },
{ id: 121, label: 'Three' },
{ id: 131, label: 'One' },
];

<Dropdown
items={items}
selectedItems={state.selectedItems}
onChange={newSelectedItems => {
setState({
selectedItems: newSelectedItems,
});
}}
>
{({ getInputProps }) => <InputText readOnly {...getInputProps()} />}
</Dropdown>;
```
Expand Down Expand Up @@ -56,7 +92,7 @@ const categories = [
items={items}
categories={categories}
twoColumn={false}
selectedItems={[items[2]]}
initialSelectedItems={[items[2]]}
>
{({ getInputProps }) => <InputText readOnly {...getInputProps()} />}
</Dropdown>;
Expand Down
31 changes: 30 additions & 1 deletion packages/react-dropdown/src/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,10 @@ it('should clear the previous selected items', () => {
</Dropdown>
);

wrapper.find('ul li').simulate('click');
wrapper
.find('ul li')
.at(0)
.simulate('click');

expect(
wrapper
Expand All @@ -577,3 +580,29 @@ it('should clear the previous selected items', () => {
.props().value
).toBe('');
});

it('onChange should be called with the newly selected items', () => {
const onChangeFn = jest.fn();

const wrapper = mount(
<Dropdown
items={items}
initialIsOpen={true}
selectedItems={[items[0]]}
onChange={onChangeFn}
>
{({ getInputProps }) => <Input {...getInputProps()} />}
</Dropdown>
);

wrapper
.find('ul li')
.hostNodes()
.at(1)
.simulate('click');

expect(onChangeFn).toHaveBeenCalledWith(
[{ id: 22, label: 'Two' }],
expect.anything()
);
});
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12061,7 +12061,7 @@ react-resize-aware@^2.7.2:
resolved "https://registry.npmjs.org/react-resize-aware/-/react-resize-aware-2.7.2.tgz#38a0040daaa28dfa9b88994889fbb1e2aa66df83"
integrity sha512-XyweQ3YTiZzNG1T5PNpCXvnpsI7mdy4A2oeySFToh8v02Yf+kOiUyWprvyp6T68fFB848w2F3RKfzH7KDk75JQ==

react-router-dom@^4.3.1:
react-router-dom@^4.0.0, react-router-dom@^4.3.1:
version "4.3.1"
resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6"
integrity sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA==
Expand Down

0 comments on commit 5928d80

Please sign in to comment.