diff --git a/README.md b/README.md index 17564e5..a0464db 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ class Example extends Component { | `responsiveHeight` | `string` | 400px | Responsive height of the wrapping component, can send percent for example: `70%` | | `withGrouping` | `boolean` | false | Your items will be grouped by the group prop values - see "item grouping" section below | | `showSelectedItemsSearch` | `boolean` | false | toggle to show search option in detination list. | +| `keepSelectionOrder` | `boolean` | false | keep selection order in the detination list | | `searchSelectedItemsValue` | `string` | | The value of the search field for destination list. | | `searchSelectedItemsChanged` | `function` | | Function to handle the change of search field for destination list. Accepts value as a single argument. | | `selectedItemsFilterFunction` | `function` | based on label | Is the same as filterFunction by default to filter items based on the search query in destination list. | diff --git a/src/components/multi_select.js b/src/components/multi_select.js index de411c6..28b2d7a 100644 --- a/src/components/multi_select.js +++ b/src/components/multi_select.js @@ -26,6 +26,7 @@ export class MultiSelect extends PureComponent { showSearch: PropTypes.bool, showSelectAll: PropTypes.bool, showSelectedItems: PropTypes.bool, + keepSelectionOrder: PropTypes.bool, searchIcon: PropTypes.string, deleteIcon: PropTypes.string, searchRenderer: PropTypes.func, @@ -55,7 +56,8 @@ export class MultiSelect extends PureComponent { loaderRenderer: Loader, withGrouping: false, generateClassName: defaultGenerateClassName, - showSelectedItemsSearch: false + showSelectedItemsSearch: false, + keepSelectionOrder: false }; calculateHeight() { @@ -112,7 +114,8 @@ export class MultiSelect extends PureComponent { searchSelectedItemsValue, filterSelectedItems, filteredSelectedItems, - isLocked + isLocked, + keepSelectionOrder } = this.props; const calculatedHeight = this.calculateHeight(); const selectedIds = selectedItems.map(item => item.id); @@ -151,6 +154,7 @@ export class MultiSelect extends PureComponent { withGrouping={withGrouping} listRenderer={listRenderer} isLocked={isLocked} + keepSelectionOrder={keepSelectionOrder} /> )} {!loading && showSelectedItems && ( diff --git a/src/components/multi_select_state.js b/src/components/multi_select_state.js index d8206b3..e7f6e69 100644 --- a/src/components/multi_select_state.js +++ b/src/components/multi_select_state.js @@ -115,7 +115,7 @@ const withMultiSelectState = WrappedComponent => } selectItem(event, id) { - const { items } = this.props; + const { items, keepSelectionOrder } = this.props; const { selectedItems, firstItemShiftSelected } = this.state; if (!selectedItems.find(item => item.id === id)) { if (event.shiftKey && firstItemShiftSelected !== undefined) { @@ -125,15 +125,25 @@ const withMultiSelectState = WrappedComponent => const index = items.findIndex(item => item.id === id); this.setState({ firstItemShiftSelected: index }); } - this.setNewItemsBySelectItem(id, items, selectedItems); + this.setNewItemsBySelectItem( + id, + items, + selectedItems, + keepSelectionOrder + ); } } else { this.unselectItems([id]); } } - setNewItemsBySelectItem(id, items, selectedItems) { - const newSelectedItems = getNewSelectedItems(id, items, selectedItems); + setNewItemsBySelectItem(id, items, selectedItems, keepSelectionOrder) { + const newSelectedItems = getNewSelectedItems( + id, + items, + selectedItems, + keepSelectionOrder + ); const newFilteredSelectedItems = this.getNewFilteredSelectedItems( newSelectedItems ); diff --git a/src/components/multi_select_state_utils.js b/src/components/multi_select_state_utils.js index 56f9f2d..13bb808 100644 --- a/src/components/multi_select_state_utils.js +++ b/src/components/multi_select_state_utils.js @@ -40,12 +40,26 @@ export const getMinMaxIndexes = (currentIndex, firstItemShiftSelected) => export const isWithin = (index, { minIndex, maxIndex }) => index >= minIndex && index <= maxIndex; -export const getNewSelectedItems = (itemId, items, selectedItems) => { - const sourceItems = items.filter( - item => item.id === itemId || findItem(item, selectedItems) - ); - const destinationItems = selectedItems.filter( - selectedItem => !findItem(selectedItem, items) - ); - return [...destinationItems, ...sourceItems]; +export const getNewSelectedItems = ( + itemId, + items, + selectedItems, + keepSelectionOrder +) => { + let alreadySelectedItems = []; + let sourceItems = []; + let destinationItems = []; + + if (keepSelectionOrder) { + // In order to keep selection order on the list, + // First, iterate threw already selectedItems + alreadySelectedItems = selectedItems.filter(item => findItem(item, items)); + sourceItems = items.filter(item => item.id === itemId); + } else { + sourceItems = items.filter( + item => item.id === itemId || findItem(item, selectedItems) + ); + } + + return [...alreadySelectedItems, ...sourceItems]; }; diff --git a/stories/multi-select.stories.js b/stories/multi-select.stories.js index 458db74..c4d2755 100644 --- a/stories/multi-select.stories.js +++ b/stories/multi-select.stories.js @@ -80,6 +80,21 @@ storiesOf("React Multi Select", module) ); }) ) + .add( + "with keep selection order", + withReadme(Readme, () => { + return ( + + ); + }) + ) .add( "With some of the items disabled", withReadme(Readme, () => { @@ -363,15 +378,12 @@ storiesOf("React Multi Select", module) withReadme(Readme, () => { class SelectedItemsController extends React.Component { SINGLE_ITEM = [{ id: 1, label: "Item 1" }]; - MULTI_ITEMS = [ - { id: 2, label: "Item 2" }, - { id: 4, label: "Item 4" } - ] + MULTI_ITEMS = [{ id: 2, label: "Item 2" }, { id: 4, label: "Item 4" }]; constructor(props) { super(props); this.state = { - selectedItems: this.SINGLE_ITEM, + selectedItems: this.SINGLE_ITEM }; } @@ -384,7 +396,10 @@ storiesOf("React Multi Select", module) type="button" onClick={() => { this.setState({ - selectedItems: this.state.selectedItems.length > 1 ? this.SINGLE_ITEM : this.MULTI_ITEMS + selectedItems: + this.state.selectedItems.length > 1 + ? this.SINGLE_ITEM + : this.MULTI_ITEMS }); }} /> diff --git a/tests/components/__snapshots__/multi_select.spec.js.snap b/tests/components/__snapshots__/multi_select.spec.js.snap index 3966e85..f43d5a3 100644 --- a/tests/components/__snapshots__/multi_select.spec.js.snap +++ b/tests/components/__snapshots__/multi_select.spec.js.snap @@ -134,6 +134,7 @@ exports[`MultiSelect Snapshots can remove search 1`] = ` isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -305,6 +306,7 @@ exports[`MultiSelect Snapshots can remove select all 1`] = ` isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -476,6 +478,7 @@ exports[`MultiSelect Snapshots can remove selected items 1`] = ` isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -629,6 +632,7 @@ exports[`MultiSelect Snapshots custom itemRenderer 1`] = ` isLocked={undefined} itemHeight={40} itemRenderer={[MockFunction mockedComponent]} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -800,6 +804,7 @@ exports[`MultiSelect Snapshots custom listRenderer 1`] = ` isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={[MockFunction mockedComponent]} messages={Object {}} noItemsRenderer={undefined} @@ -971,6 +976,7 @@ exports[`MultiSelect Snapshots custom messages 1`] = ` isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={ Object { @@ -1160,6 +1166,7 @@ exports[`MultiSelect Snapshots custom noItemsRenderer 1`] = ` isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={[MockFunction mockedComponent]} @@ -1331,6 +1338,7 @@ exports[`MultiSelect Snapshots custom searchRenderer 1`] = ` isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -1502,6 +1510,7 @@ exports[`MultiSelect Snapshots custom selectAllRenderer 1`] = ` isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -1673,6 +1682,7 @@ exports[`MultiSelect Snapshots default snapshot 1`] = ` isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -2102,6 +2112,7 @@ exports[`MultiSelect Snapshots does not pass disabled if maxSelectedItem has pas isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -2283,6 +2294,7 @@ exports[`MultiSelect Snapshots passed clearAll 1`] = ` isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -2454,6 +2466,7 @@ exports[`MultiSelect Snapshots passed filterItems 1`] = ` isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -2625,6 +2638,7 @@ exports[`MultiSelect Snapshots passed selectAllItems 1`] = ` isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -2796,6 +2810,7 @@ exports[`MultiSelect Snapshots passed selectItem 1`] = ` isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -2967,6 +2982,7 @@ exports[`MultiSelect Snapshots passed selectedIds 1`] = ` isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -3138,6 +3154,7 @@ exports[`MultiSelect Snapshots passed selectedItems 1`] = ` isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -3319,6 +3336,7 @@ exports[`MultiSelect Snapshots passed unselectItems 1`] = ` isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -3490,6 +3508,7 @@ exports[`MultiSelect Snapshots passes disabled if maxSelectedItem has passed 1`] isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -3671,6 +3690,7 @@ exports[`MultiSelect Snapshots will pass itemHeight 1`] = ` isLocked={undefined} itemHeight={60} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -3842,6 +3862,7 @@ exports[`MultiSelect Snapshots will pass selectAllHeight 1`] = ` isLocked={undefined} itemHeight={60} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -4013,6 +4034,7 @@ exports[`MultiSelect Snapshots will pass selectAllHeight without itemHeight 1`] isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -4184,6 +4206,7 @@ exports[`MultiSelect Snapshots will pass selectedItemHeight 1`] = ` isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -4355,6 +4378,7 @@ exports[`MultiSelect Snapshots with generateClassName 1`] = ` isLocked={undefined} itemHeight={40} itemRenderer={undefined} + keepSelectionOrder={false} listRenderer={undefined} messages={Object {}} noItemsRenderer={undefined} @@ -4439,6 +4463,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [Function], @@ -4468,6 +4493,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [Function], @@ -4497,6 +4523,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [Function], @@ -4526,6 +4553,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [MockFunction mockedComponent], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [Function], @@ -4555,6 +4583,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [MockFunction mockedComponent], "messages": Object {}, "noItemsRenderer": [Function], @@ -4584,6 +4613,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object { "clearAllMessage": "Uncheck all", @@ -4620,6 +4650,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [MockFunction mockedComponent], @@ -4649,6 +4680,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [Function], @@ -4678,6 +4710,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [Function], @@ -4707,6 +4740,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [Function], @@ -4736,6 +4770,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [Function], @@ -4768,6 +4803,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [Function], @@ -4797,6 +4833,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [Function], @@ -4826,6 +4863,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [Function], @@ -4855,6 +4893,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [Function], @@ -4884,6 +4923,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [Function], @@ -4913,6 +4953,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [Function], @@ -4945,6 +4986,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [Function], @@ -4974,6 +5016,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [Function], @@ -5006,6 +5049,7 @@ Object { "isLocked": undefined, "itemHeight": 60, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [Function], @@ -5035,6 +5079,7 @@ Object { "isLocked": undefined, "itemHeight": 60, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [Function], @@ -5064,6 +5109,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [Function], @@ -5093,6 +5139,7 @@ Object { "isLocked": undefined, "itemHeight": 40, "itemRenderer": [Function], + "keepSelectionOrder": false, "listRenderer": [Function], "messages": Object {}, "noItemsRenderer": [Function], diff --git a/tests/components/multi_select_state.spec.js b/tests/components/multi_select_state.spec.js index a9a18fc..ad97047 100644 --- a/tests/components/multi_select_state.spec.js +++ b/tests/components/multi_select_state.spec.js @@ -17,6 +17,7 @@ const EVENT_WITH_SHIFT = { keyCode: 16, shiftKey: true }; const EVENT_WITH_CTRL = { keyCode: 17, shiftKey: true }; const items = [ITEM_1, ITEM_2, ITEM_3]; +const manyItems = [ITEM_1, ITEM_2, ITEM_3, ITEM_4, ITEM_12, ITEM_22]; const itemsWithDisabled = [ITEM_1, ITEM_2, DISABLED_ITEM_23, ITEM_3]; describe("withMultiSelectState", () => { @@ -179,7 +180,58 @@ describe("withMultiSelectState", () => { wrapper.update(); wrapper.props().selectItem(EVENT, ITEM_1.id); wrapper.update(); - expect(wrapper.prop("selectedItems")).toEqual([ITEM_1, ITEM_2]); + wrapper.props().selectItem(EVENT, ITEM_3.id); + wrapper.update(); + expect(wrapper.prop("selectedItems")).toEqual([ITEM_1, ITEM_2, ITEM_3]); + }); + + test("keep selection order", () => { + const ConditionalComponent = withMultiSelectState(CustomComponent); + const wrapper = shallow( + + ); + wrapper.props().selectItem(EVENT, ITEM_2.id); + wrapper.update(); + wrapper.props().selectItem(EVENT, ITEM_1.id); + wrapper.update(); + wrapper.props().selectItem(EVENT, ITEM_3.id); + wrapper.update(); + wrapper.props().selectItem(EVENT, ITEM_22.id); + wrapper.update(); + wrapper.props().selectItem(EVENT, ITEM_12.id); + wrapper.update(); + expect(wrapper.prop("selectedItems")).toEqual([ + ITEM_2, + ITEM_1, + ITEM_3, + ITEM_22, + ITEM_12 + ]); + }); + + test("select some items with selection order and then click on select all", () => { + const ConditionalComponent = withMultiSelectState(CustomComponent); + const wrapper = shallow( + + ); + wrapper.props().selectItem(EVENT, ITEM_2.id); + wrapper.update(); + wrapper.props().selectItem(EVENT, ITEM_1.id); + wrapper.update(); + wrapper.props().selectItem(EVENT, ITEM_22.id); + wrapper.update(); + wrapper.props().selectItem(EVENT, ITEM_3.id); + wrapper.update(); + wrapper.props().selectAllItems(); + wrapper.update(); + expect(wrapper.prop("selectedItems")).toEqual([ + ITEM_1, + ITEM_2, + ITEM_3, + ITEM_4, + ITEM_12, + ITEM_22 + ]); }); test("can filter items", () => {