UNPKG

rsuite

Version:

A suite of react components

367 lines (356 loc) 10.7 kB
'use client'; import _extends from "@babel/runtime/helpers/esm/extends"; import React, { useCallback, useMemo } from 'react'; import pick from 'lodash/pick'; import isNil from 'lodash/isNil'; import isFunction from 'lodash/isFunction'; import TreeView from "../CascadeTree/TreeView.js"; import SearchView from "../CascadeTree/SearchView.js"; import { usePaths, useSelect, useSearch } from "../CascadeTree/hooks/index.js"; import { flattenTree } from "../Tree/utils/index.js"; import { findNodeOfTree, getParentMap } from "../internals/Tree/utils/index.js"; import { useControlled, useStyles, useCustom, useEventCallback, useMap } from "../internals/hooks/index.js"; import { forwardRef, createChainedFunction, mergeRefs, shallowEqual } from "../internals/utils/index.js"; import { PickerToggle, PickerPopup, PickerToggleTrigger, usePickerRef, useToggleKeyDownEvent, useFocusItemValue, triggerPropKeys } from "../internals/Picker/index.js"; import useActive from "./useActive.js"; const emptyArray = []; /** * The `Cascader` component displays a hierarchical list of options. * @see https://rsuitejs.com/components/cascader */ const Cascader = forwardRef((props, ref) => { const { rtl, propsWithDefaults } = useCustom('Cascader', props); const { appearance = 'default', as, block, className, cleanable = true, classPrefix = 'picker', columnHeight, columnWidth, data = emptyArray, defaultValue, disabled, disabledItemValues = emptyArray, childrenKey = 'children', id, labelKey = 'label', locale, parentSelectable, placeholder, placement = 'bottomStart', popupClassName, popupStyle, renderColumn, renderExtraFooter, renderSearchItem, renderTreeNode, renderValue, searchable = true, style, toggleAs, value: valueProp, valueKey = 'value', onClean, onChange, onEnter, onExit, onSearch, onSelect, getChildren, ...rest } = propsWithDefaults; const { trigger, root, target, overlay, searchInput } = usePickerRef(ref); const [value, setValue] = useControlled(valueProp, defaultValue); // Store the children of each node const childrenMap = useMap(); // Store the parent of each node const parentMap = useMemo(() => getParentMap(data, item => childrenMap.get(item) ?? item[childrenKey]), [childrenMap, childrenKey, data]); // Flatten the tree data const flattenedData = useMemo(() => flattenTree(data, item => childrenMap.get(item) ?? item[childrenKey]), [childrenMap, childrenKey, data]); // The selected item const selectedItem = flattenedData.find(item => item[valueKey] === value); // Callback function after selecting the node const onSelectCallback = (node, event) => { const { isLeafNode, cascadePaths, itemData } = node; onSelect?.(itemData, cascadePaths, event); const nextValue = itemData[valueKey]; if (isLeafNode) { // Determines whether the option is a leaf node, and if so, closes the picker. handleClose(); setValue(nextValue); return; } // When the parent is optional, the value and the displayed path are updated. if (parentSelectable && !shallowEqual(value, nextValue)) { setValue(nextValue); onChange?.(nextValue, event); } // Update menu position trigger.current?.updatePosition(); }; const { activeItem, setActiveItem, loadingItemsSet, handleSelect } = useSelect({ value, valueKey, childrenKey, childrenMap, selectedItem, getChildren, onChange, onSelect: onSelectCallback }); const { columns, pathTowardsActiveItem, pathTowardsSelectedItem } = usePaths({ data, activeItem, selectedItem, getParent: item => parentMap.get(item), getChildren: item => childrenMap.get(item) ?? item[childrenKey] }); /** * 1.Have a value and the value is valid. * 2.Regardless of whether the value is valid, as long as renderValue is set, it is judged to have a value. */ let hasValue = pathTowardsSelectedItem.length > 0 || !isNil(value) && isFunction(renderValue); const { prefix, merge } = useStyles(classPrefix); const onFocusItemCallback = useCallback(value => { setActiveItem(flattenedData.find(item => item[valueKey] === value)); }, [flattenedData, setActiveItem, valueKey]); // Used to hover the focuse item when trigger `onKeydown` const { focusItemValue, setFocusItemValue, setLayer, setKeys, onKeyDown: onFocusItem } = useFocusItemValue(value, { rtl, data: flattenedData, valueKey, defaultLayer: pathTowardsSelectedItem?.length ? pathTowardsSelectedItem.length - 1 : 0, target: () => overlay.current, getParent: item => parentMap.get(item), callback: onFocusItemCallback }); const onSearchCallback = (value, items, event) => { onSearch?.(value, event); if (!value || items.length === 0) { setFocusItemValue(undefined); return; } if (items.length > 0) { setFocusItemValue(items[0][valueKey]); setLayer(0); setKeys([]); } }; const { items, searchKeyword, setSearchKeyword, handleSearch } = useSearch({ labelKey, childrenKey, parentMap, flattenedData, parentSelectable, onSearch: onSearchCallback }); const { active, events } = useActive({ onEnter, onExit, target, setSearchKeyword }); const handleClose = useEventCallback(() => { trigger.current?.close(); // The focus is on the trigger button after closing target.current?.focus?.(); }); const handleClean = useEventCallback(event => { if (disabled || !target.current) { return; } setValue(null); onChange?.(null, event); }); const handleMenuPressEnter = useEventCallback(event => { const focusItem = findNodeOfTree(data, item => item[valueKey] === focusItemValue); const isLeafNode = focusItem && !focusItem[childrenKey]; if (isLeafNode) { setValue(focusItemValue); if (pathTowardsActiveItem.length) { setLayer(pathTowardsActiveItem.length - 1); } if (!shallowEqual(value, focusItemValue)) { onSelect?.(focusItem, pathTowardsActiveItem, event); onChange?.(focusItemValue ?? null, event); } handleClose(); } }); const onPickerKeyDown = useToggleKeyDownEvent({ toggle: !focusItemValue || !active, trigger, target, overlay, searchInput, active, onExit: handleClean, onMenuKeyDown: onFocusItem, onMenuPressEnter: handleMenuPressEnter, ...rest }); /** * The search structure option is processed after being selected. */ const handleSearchRowSelect = useEventCallback((itemData, nodes, event) => { const nextValue = itemData[valueKey]; handleClose(); setSearchKeyword(''); setValue(nextValue); onSelect?.(itemData, nodes, event); onChange?.(nextValue, event); }); const renderCascadeColumn = (childNodes, column) => { if (typeof renderColumn === 'function') { return renderColumn(childNodes, column); } return childNodes; }; const renderCascadeTreeNode = (node, itemData) => { if (typeof renderTreeNode === 'function') { return renderTreeNode(node, itemData); } return node; }; const renderTreeView = (positionProps, speakerRef) => { const { className } = positionProps || {}; const classes = merge(className, popupClassName, prefix('popup-cascader')); return /*#__PURE__*/React.createElement(PickerPopup, { ref: mergeRefs(overlay, speakerRef), className: classes, style: popupStyle, target: trigger, onKeyDown: onPickerKeyDown }, searchable && /*#__PURE__*/React.createElement(SearchView, { data: items, searchKeyword: searchKeyword, valueKey: valueKey, labelKey: labelKey, locale: locale, parentMap: parentMap, disabledItemValues: disabledItemValues, focusItemValue: focusItemValue, inputRef: searchInput, renderSearchItem: renderSearchItem, onSelect: handleSearchRowSelect, onSearch: handleSearch }), searchKeyword === '' && /*#__PURE__*/React.createElement(TreeView, { columnWidth: columnWidth, columnHeight: columnHeight, disabledItemValues: disabledItemValues, loadingItemsSet: loadingItemsSet, valueKey: valueKey, labelKey: labelKey, childrenKey: childrenKey, classPrefix: 'cascade-tree', data: columns, cascadePaths: pathTowardsActiveItem, activeItemValue: value, onSelect: handleSelect, renderColumn: renderCascadeColumn, renderTreeNode: renderCascadeTreeNode }), renderExtraFooter?.()); }; let selectedElement = placeholder; if (pathTowardsSelectedItem.length > 0) { selectedElement = []; pathTowardsSelectedItem.forEach((item, index) => { const key = item[valueKey] || item[labelKey]; selectedElement.push(/*#__PURE__*/React.createElement("span", { key: key }, item[labelKey])); if (index < pathTowardsSelectedItem.length - 1) { selectedElement.push(/*#__PURE__*/React.createElement("span", { className: "separator", key: `${key}-separator` }, ' / ')); } }); } if (!isNil(value) && isFunction(renderValue)) { selectedElement = renderValue(value, pathTowardsSelectedItem, selectedElement); // If renderValue returns null or undefined, hasValue is false. if (isNil(selectedElement)) { hasValue = false; } } const triggerProps = { ...pick(props, triggerPropKeys), ...events }; return /*#__PURE__*/React.createElement(PickerToggleTrigger, { as: as, id: id, pickerType: "cascader", block: block, disabled: disabled, appearance: appearance, popupType: "tree", triggerProps: triggerProps, ref: trigger, placement: placement, speaker: renderTreeView, rootRef: root, style: style, classPrefix: classPrefix, className: className }, /*#__PURE__*/React.createElement(PickerToggle, _extends({ ref: target, as: toggleAs, appearance: appearance, disabled: disabled, onClean: createChainedFunction(handleClean, onClean), onKeyDown: onPickerKeyDown, cleanable: cleanable && !disabled, hasValue: hasValue, active: active, placement: placement, inputValue: value ?? '', focusItemValue: focusItemValue }, rest), selectedElement || locale?.placeholder)); }); Cascader.displayName = 'Cascader'; export default Cascader;