diff --git a/.all-contributorsrc b/.all-contributorsrc index a74b62ac4ba7..0129e6c6cab2 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -848,6 +848,15 @@ "contributions": [ "code" ] + }, + { + "login": "dezkareid", + "name": "Joel Humberto Gómez Paredes", + "avatar_url": "https://avatars.githubusercontent.com/u/1269896?v=4", + "profile": "https://github.com/dezkareid", + "contributions": [ + "code" + ] } ], "commitConvention": "none" diff --git a/README.md b/README.md index eb72d8a9552b..13bd0c5821e7 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,7 @@ check out our [Contributing Guide](/.github/CONTRIBUTING.md) and our
Zhen Wang

💻 📖
Cathal Kenneally

💻 +
Joel Humberto Gómez Paredes

💻 diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 29e416cc166e..cedc2c0944cd 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -4311,6 +4311,7 @@ Map { "light": false, "locale": "en", "open": false, + "selectedItems": null, "selectionFeedback": "top-after-reopen", "sortItems": [Function], "title": false, @@ -4507,6 +4508,9 @@ Map { "open": Object { "type": "bool", }, + "selectedItems": Object { + "type": "array", + }, "selectionFeedback": Object { "args": Array [ Array [ diff --git a/packages/react/src/components/MultiSelect/MultiSelect-story.js b/packages/react/src/components/MultiSelect/MultiSelect-story.js index 5872c25c574d..146ce9a30430 100644 --- a/packages/react/src/components/MultiSelect/MultiSelect-story.js +++ b/packages/react/src/components/MultiSelect/MultiSelect-story.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { action } from '@storybook/addon-actions'; import { withKnobs, @@ -20,6 +20,7 @@ import MultiSelect from '../MultiSelect'; import FilterableMultiSelect from '../MultiSelect/FilterableMultiSelect'; import Checkbox from '../Checkbox'; import mdx from './MultiSelect.mdx'; +import Button from '../Button'; const items = [ { @@ -143,6 +144,44 @@ export const Default = withReadme(readme, () => { ); }); +export const controlled = withReadme(readme, () => { + const { + listBoxMenuIconTranslationIds, + selectionFeedback, + ...multiSelectProps + } = props(); + const [selectedItems, setSelectedItems] = useState([]); + const onChange = useCallback(({ selectedItems: newSelectedItems }) => { + setSelectedItems(newSelectedItems); + }, []); + + return ( +
+ (item ? item.text : '')} + translateWithId={(id) => listBoxMenuIconTranslationIds[id]} + selectionFeedback={selectionFeedback} + onChange={onChange} + selectedItems={selectedItems} + /> + + +
+ ); +}); + export const ItemToElement = withReadme(readme, () => { return (
diff --git a/packages/react/src/components/MultiSelect/MultiSelect.js b/packages/react/src/components/MultiSelect/MultiSelect.js index c9f0e96d6cd7..42e85a25c686 100644 --- a/packages/react/src/components/MultiSelect/MultiSelect.js +++ b/packages/react/src/components/MultiSelect/MultiSelect.js @@ -68,6 +68,7 @@ const MultiSelect = React.forwardRef(function MultiSelect( onChange, onMenuChange, direction, + selectedItems: selected, }, ref ) { @@ -85,6 +86,7 @@ const MultiSelect = React.forwardRef(function MultiSelect( disabled, initialSelectedItems, onChange, + selectedItems: selected, }); const { @@ -431,6 +433,11 @@ MultiSelect.propTypes = { */ open: PropTypes.bool, + /** + * For full control of the selected items + */ + selectedItems: PropTypes.array, + /** * Specify feedback (mode) of the selection. * `top`: selected item jumps to top @@ -491,6 +498,7 @@ MultiSelect.defaultProps = { direction: 'bottom', clearSelectionText: 'To clear selection, press Delete or Backspace,', clearSelectionDescription: 'Total items selected: ', + selectedItems: null, }; export default MultiSelect; diff --git a/packages/react/src/internal/Selection.js b/packages/react/src/internal/Selection.js index a3c1a3ce576f..5995f5e9421d 100644 --- a/packages/react/src/internal/Selection.js +++ b/packages/react/src/internal/Selection.js @@ -9,14 +9,35 @@ import React, { useCallback, useEffect, useState, useRef } from 'react'; import PropTypes from 'prop-types'; import isEqual from 'lodash.isequal'; +function callOnChangeHandler({ + isControlled, + isMounted, + onChangeHandlerControlled, + onChangeHandlerUncontrolled, + selectedItems, +}) { + if (isControlled) { + if (isMounted && onChangeHandlerControlled) { + onChangeHandlerControlled({ selectedItems }); + } + } else { + onChangeHandlerUncontrolled(selectedItems); + } +} + export function useSelection({ disabled, onChange, initialSelectedItems = [], + selectedItems: controlledItems, }) { const isMounted = useRef(false); const savedOnChange = useRef(onChange); - const [selectedItems, setSelectedItems] = useState(initialSelectedItems); + const [uncontrolledItems, setUncontrolledItems] = useState( + initialSelectedItems + ); + const isControlled = !!controlledItems; + const selectedItems = isControlled ? controlledItems : uncontrolledItems; const onItemChange = useCallback( (item) => { if (disabled) { @@ -29,35 +50,53 @@ export function useSelection({ selectedIndex = index; } }); - + let newSelectedItems; if (selectedIndex === undefined) { - setSelectedItems((selectedItems) => selectedItems.concat(item)); + newSelectedItems = selectedItems.concat(item); + callOnChangeHandler({ + isControlled, + isMounted: isMounted.current, + onChangeHandlerControlled: savedOnChange.current, + onChangeHandlerUncontrolled: setUncontrolledItems, + selectedItems: newSelectedItems, + }); return; } - setSelectedItems((selectedItems) => - removeAtIndex(selectedItems, selectedIndex) - ); + newSelectedItems = removeAtIndex(selectedItems, selectedIndex); + callOnChangeHandler({ + isControlled, + isMounted: isMounted.current, + onChangeHandlerControlled: savedOnChange.current, + onChangeHandlerUncontrolled: setUncontrolledItems, + selectedItems: newSelectedItems, + }); }, - [disabled, selectedItems] + [disabled, isControlled, selectedItems] ); const clearSelection = useCallback(() => { if (disabled) { return; } - setSelectedItems([]); - }, [disabled]); + callOnChangeHandler({ + isControlled, + isMounted: isMounted.current, + onChangeHandlerControlled: savedOnChange.current, + onChangeHandlerUncontrolled: setUncontrolledItems, + selectedItems: [], + }); + }, [disabled, isControlled]); useEffect(() => { savedOnChange.current = onChange; }, [onChange]); useEffect(() => { - if (isMounted.current === true && savedOnChange.current) { + if (isMounted.current && savedOnChange.current && !isControlled) { savedOnChange.current({ selectedItems }); } - }, [selectedItems]); + }, [isControlled, selectedItems]); useEffect(() => { isMounted.current = true;