UNPKG

@carbon/react

Version:

React components for the Carbon Design System

137 lines (130 loc) 4.39 kB
/** * Copyright IBM Corp. 2016, 2023 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ import { useRef, useState, useCallback, useEffect } from 'react'; import isEqual from 'react-fast-compare'; const callOnChangeHandler = ({ isControlled, isMounted, onChangeHandlerControlled, onChangeHandlerUncontrolled, selectedItems }) => { if (isControlled) { if (isMounted && onChangeHandlerControlled) { // Use setTimeout to defer the controlled onChange call, // avoiding React’s warning: "Cannot update a component while rendering a different component". // This ensures the parent state updates after rendering completes. setTimeout(() => { onChangeHandlerControlled({ selectedItems }); }, 0); } } else { onChangeHandlerUncontrolled(selectedItems); } }; const useSelection = ({ disabled, onChange, initialSelectedItems = [], selectedItems: controlledItems, selectAll = false, filteredItems = [] }) => { const isMounted = useRef(false); const savedOnChange = useRef(onChange); const [uncontrolledItems, setUncontrolledItems] = useState(initialSelectedItems); const isControlled = !!controlledItems; const selectedItems = isControlled ? controlledItems : uncontrolledItems; const onItemChange = useCallback(item => { if (disabled) return; // Assert special properties (e.g., `disabled`, `isSelectAll`, etc.) are // `any` since those properties aren’t part of the generic type. const allSelectableItems = filteredItems.filter(item => !item?.disabled && !item?.isSelectAll); const disabledItemCount = filteredItems.filter(item => item?.disabled).length; let newSelectedItems; // deselect all on click to All/indeterminate option if (item?.isSelectAll && selectedItems.length > 0) { newSelectedItems = []; } // select all options else if (item?.isSelectAll && selectedItems.length === 0) { newSelectedItems = allSelectableItems; } else { const selectedIndex = selectedItems.findLastIndex(selectedItem => isEqual(selectedItem, item)); if (selectedIndex === -1) { newSelectedItems = selectedItems.concat(item); // checking if all items are selected then We should select mark the 'select All' option as well if (selectAll && filteredItems.length - 1 === newSelectedItems.length + disabledItemCount) { newSelectedItems = allSelectableItems; } } else { newSelectedItems = removeAtIndex(selectedItems, selectedIndex); newSelectedItems = newSelectedItems.filter(item => !item?.isSelectAll); } } callOnChangeHandler({ isControlled, isMounted: isMounted.current, onChangeHandlerControlled: savedOnChange.current, onChangeHandlerUncontrolled: setUncontrolledItems, selectedItems: newSelectedItems }); }, [disabled, selectedItems, filteredItems, selectAll, isControlled]); const clearSelection = useCallback(() => { if (disabled) return; callOnChangeHandler({ isControlled, isMounted: isMounted.current, onChangeHandlerControlled: savedOnChange.current, onChangeHandlerUncontrolled: setUncontrolledItems, selectedItems: [] }); }, [disabled, isControlled]); const toggleAll = useCallback(items => { callOnChangeHandler({ isControlled, isMounted: isMounted.current, onChangeHandlerControlled: savedOnChange.current, onChangeHandlerUncontrolled: setUncontrolledItems, selectedItems: items }); }, [isControlled]); useEffect(() => { savedOnChange.current = onChange; }, [onChange]); useEffect(() => { if (isMounted.current && savedOnChange.current && !isControlled) { savedOnChange.current({ selectedItems }); } }, [isControlled, selectedItems]); useEffect(() => { isMounted.current = true; return () => { isMounted.current = false; }; }, []); return { clearSelection, onItemChange, toggleAll, selectedItems }; }; /** * Generic utility for safely removing an element at a given index from an * array. */ const removeAtIndex = (array, index) => { const result = array.slice(); result.splice(index, 1); return result; }; export { useSelection };