rsuite
Version:
A suite of react components
290 lines (288 loc) • 8.72 kB
JavaScript
'use client';
import _extends from "@babel/runtime/helpers/esm/extends";
import React, { useMemo } from 'react';
import pick from 'lodash/pick';
import isNil from 'lodash/isNil';
import isFunction from 'lodash/isFunction';
import useTreeWithChildren from "../Tree/hooks/useTreeWithChildren.js";
import useFlattenTree from "../Tree/hooks/useFlattenTree.js";
import useFocusState from "./hooks/useFocusState.js";
import useExpandTree from "../Tree/hooks/useExpandTree.js";
import TreeView from "../Tree/TreeView.js";
import { useStyles, useCustom, useControlled, useEventCallback } from "../internals/hooks/index.js";
import { forwardRef, createChainedFunction, mergeRefs } from "../internals/utils/index.js";
import { getActiveItem, getTreeActiveNode } from "../Tree/utils/index.js";
import { PickerToggle, PickerPopup, PickerToggleTrigger, usePickerRef, onMenuKeyDown, triggerPropKeys, useToggleKeyDownEvent } from "../internals/Picker/index.js";
import { isLeafNode } from "../internals/Tree/utils/index.js";
import { TreeProvider, useTreeImperativeHandle } from "../internals/Tree/TreeProvider.js";
/**
* The `TreePicker` component is used for selecting single options which are organized in a tree structure.
*
* @see https://rsuitejs.com/components/tree-picker/
*/
const TreePicker = forwardRef((props, ref) => {
const {
propsWithDefaults
} = useCustom('TreePicker', props);
const {
as,
appearance = 'default',
classPrefix = 'picker',
cleanable = true,
childrenKey = 'children',
data = [],
disabled,
defaultValue,
defaultExpandAll = false,
disabledItemValues = [],
defaultExpandItemValues = [],
expandItemValues: controlledExpandItemValues,
id,
block,
className,
locale,
labelKey = 'label',
onlyLeafSelectable,
placeholder,
placement = 'bottomStart',
style,
searchKeyword,
searchable = true,
showIndentLine,
popupClassName,
popupStyle,
popupAutoWidth = true,
treeHeight = 320,
valueKey = 'value',
virtualized = false,
value: controlledValue,
listProps,
toggleAs,
searchBy,
getChildren,
onClean,
onSearch,
onSelect,
onSelectItem,
onChange,
onExpand,
onEnter,
onExit,
onEntered,
renderValue,
renderTree,
renderTreeIcon,
renderTreeNode,
renderExtraFooter,
...rest
} = propsWithDefaults;
const {
trigger,
root,
target,
overlay,
list,
searchInput,
treeView
} = usePickerRef(ref);
const [value, setValue] = useControlled(controlledValue, defaultValue);
const itemDataKeys = {
childrenKey,
labelKey,
valueKey
};
const {
treeData,
loadingNodeValues,
appendChild
} = useTreeWithChildren(data, itemDataKeys);
const flattenedNodes = useFlattenTree(treeData, {
...itemDataKeys
});
const {
expandItemValues,
handleExpandTreeNode
} = useExpandTree(data, {
...itemDataKeys,
defaultExpandAll,
defaultExpandItemValues,
controlledExpandItemValues,
onExpand,
getChildren,
appendChild
});
const {
prefix,
merge
} = useStyles(classPrefix);
const activeNode = getTreeActiveNode(flattenedNodes, value, valueKey);
const {
register,
focusFirstNode,
focusActiveNode
} = useTreeImperativeHandle();
const {
active,
focusItemValue,
setFocusItemValue,
triggerProps
} = useFocusState({
focusActiveNode,
target,
value,
onEnter,
onExit,
onEntered
});
const handleSelect = useEventCallback((treeNode, value, event) => {
var _target$current, _trigger$current, _trigger$current$clos;
onSelect === null || onSelect === void 0 || onSelect(treeNode, value, event);
// Only leaf nodes can update the value and close the picker.
if (onlyLeafSelectable && !isLeafNode(treeNode)) {
return;
}
setFocusItemValue(value);
handleChange(value, event);
(_target$current = target.current) === null || _target$current === void 0 || _target$current.focus();
(_trigger$current = trigger.current) === null || _trigger$current === void 0 || (_trigger$current$clos = _trigger$current.close) === null || _trigger$current$clos === void 0 || _trigger$current$clos.call(_trigger$current);
});
const handleClean = useEventCallback(event => {
const target = event.target;
// exclude searchbox
if (target.matches('input[role="searchbox"]') || disabled || !cleanable) {
return;
}
setValue(null);
onChange === null || onChange === void 0 || onChange(null, event);
});
const handleTreePressEnter = useEventCallback(event => {
if (isNil(focusItemValue)) {
return;
}
const activeItem = getActiveItem(focusItemValue, flattenedNodes, valueKey);
handleSelect(activeItem, focusItemValue, event);
});
const handleTreeKeyDown = useEventCallback(event => {
onMenuKeyDown(event, {
del: handleClean,
down: () => focusFirstNode(),
enter: handleTreePressEnter
});
});
const onPickerKeydown = useToggleKeyDownEvent({
toggle: !activeNode || !active,
trigger,
target,
overlay,
searchInput,
active,
onExit: handleClean,
onMenuKeyDown: handleTreeKeyDown,
...rest
});
const handleChange = useEventCallback((nextValue, event) => {
setValue(nextValue);
onChange === null || onChange === void 0 || onChange(nextValue, event);
});
const treeContext = useMemo(() => ({
register,
props: {
labelKey,
valueKey,
childrenKey,
virtualized,
renderTreeIcon,
renderTreeNode
}
}), [childrenKey, labelKey, valueKey, virtualized, register, renderTreeIcon, renderTreeNode]);
const tree = /*#__PURE__*/React.createElement(TreeProvider, {
value: treeContext
}, /*#__PURE__*/React.createElement(TreeView, {
ref: treeView,
value: value,
data: treeData,
disabledItemValues: disabledItemValues,
expandItemValues: expandItemValues,
showIndentLine: showIndentLine,
searchable: searchable,
searchKeyword: searchKeyword,
searchBy: searchBy,
searchInputRef: searchInput,
loadingNodeValues: loadingNodeValues,
flattenedNodes: flattenedNodes,
listProps: listProps,
listRef: list,
locale: locale,
height: treeHeight,
onExpand: handleExpandTreeNode,
onSearch: onSearch,
onSelect: handleSelect,
onSelectItem: onSelectItem,
onFocusItem: setFocusItemValue
}));
const renderTreeView = (positionProps, speakerRef) => {
const {
className
} = positionProps;
const classes = merge(className, popupClassName, prefix('tree-menu'));
return /*#__PURE__*/React.createElement(PickerPopup, {
autoWidth: popupAutoWidth,
className: classes,
style: popupStyle,
ref: mergeRefs(overlay, speakerRef),
onKeyDown: onPickerKeydown,
target: trigger
}, renderTree ? renderTree(tree) : tree, renderExtraFooter === null || renderExtraFooter === void 0 ? void 0 : renderExtraFooter());
};
/**
* 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 hasValidValue = !isNil(activeNode) || !isNil(value) && isFunction(renderValue);
let selectedElement = placeholder;
if (hasValidValue) {
const node = activeNode !== null && activeNode !== void 0 ? activeNode : {};
selectedElement = node[labelKey];
if (isFunction(renderValue) && value) {
selectedElement = renderValue(value, node, selectedElement);
if (isNil(selectedElement)) {
hasValidValue = false;
}
}
}
return /*#__PURE__*/React.createElement(PickerToggleTrigger, {
as: as,
id: id,
pickerType: "tree",
block: block,
disabled: disabled,
appearance: appearance,
popupType: "tree",
triggerProps: {
...pick(props, triggerPropKeys),
...triggerProps
},
ref: trigger,
placement: placement,
speaker: renderTreeView,
rootRef: root,
style: style,
classPrefix: classPrefix,
className: className
}, /*#__PURE__*/React.createElement(PickerToggle, _extends({
ref: target,
appearance: appearance,
onKeyDown: onPickerKeydown,
onClean: createChainedFunction(handleClean, onClean),
cleanable: cleanable && !disabled,
as: toggleAs,
disabled: disabled,
hasValue: hasValidValue,
active: active,
placement: placement,
inputValue: value,
focusItemValue: focusItemValue
}, rest), selectedElement || (locale === null || locale === void 0 ? void 0 : locale.placeholder)));
});
TreePicker.displayName = 'TreePicker';
export default TreePicker;