@carbon/react
Version:
React components for the Carbon Design System
137 lines (130 loc) • 4.39 kB
JavaScript
/**
* 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 };