ant-design-vue/components/vc-cascader2/OptionList/index.tsx

214 lines
6.2 KiB
Vue

/* eslint-disable default-case */
import * as React from 'react';
import classNames from 'classnames';
import { useBaseProps } from 'rc-select';
import type { RefOptionListProps } from 'rc-select/lib/OptionList';
import Column from './Column';
import CascaderContext from '../context';
import type { DefaultOptionType, SingleValueType } from '../Cascader';
import { isLeaf, toPathKey, toPathKeys, toPathValueStr } from '../utils/commonUtil';
import useActive from './useActive';
import useKeyboard from './useKeyboard';
import { toPathOptions } from '../utils/treeUtil';
const RefOptionList = React.forwardRef<RefOptionListProps>((props, ref) => {
const { prefixCls, multiple, searchValue, toggleOpen, notFoundContent, direction } =
useBaseProps();
const containerRef = React.useRef<HTMLDivElement>();
const rtl = direction === 'rtl';
const {
options,
values,
halfValues,
fieldNames,
changeOnSelect,
onSelect,
searchOptions,
dropdownPrefixCls,
loadData,
expandTrigger,
} = React.useContext(CascaderContext);
const mergedPrefixCls = dropdownPrefixCls || prefixCls;
// ========================= loadData =========================
const [loadingKeys, setLoadingKeys] = React.useState([]);
const internalLoadData = (valueCells: React.Key[]) => {
// Do not load when search
if (!loadData || searchValue) {
return;
}
const optionList = toPathOptions(valueCells, options, fieldNames);
const rawOptions = optionList.map(({ option }) => option);
const lastOption = rawOptions[rawOptions.length - 1];
if (lastOption && !isLeaf(lastOption, fieldNames)) {
const pathKey = toPathKey(valueCells);
setLoadingKeys(keys => [...keys, pathKey]);
loadData(rawOptions);
}
};
// zombieJ: This is bad. We should make this same as `rc-tree` to use Promise instead.
React.useEffect(() => {
if (loadingKeys.length) {
loadingKeys.forEach(loadingKey => {
const valueStrCells = toPathValueStr(loadingKey);
const optionList = toPathOptions(valueStrCells, options, fieldNames, true).map(
({ option }) => option,
);
const lastOption = optionList[optionList.length - 1];
if (!lastOption || lastOption[fieldNames.children] || isLeaf(lastOption, fieldNames)) {
setLoadingKeys(keys => keys.filter(key => key !== loadingKey));
}
});
}
}, [options, loadingKeys, fieldNames]);
// ========================== Values ==========================
const checkedSet = React.useMemo(() => new Set(toPathKeys(values)), [values]);
const halfCheckedSet = React.useMemo(() => new Set(toPathKeys(halfValues)), [halfValues]);
// ====================== Accessibility =======================
const [activeValueCells, setActiveValueCells] = useActive();
// =========================== Path ===========================
const onPathOpen = (nextValueCells: React.Key[]) => {
setActiveValueCells(nextValueCells);
// Trigger loadData
internalLoadData(nextValueCells);
};
const isSelectable = (option: DefaultOptionType) => {
const { disabled } = option;
const isMergedLeaf = isLeaf(option, fieldNames);
return !disabled && (isMergedLeaf || changeOnSelect || multiple);
};
const onPathSelect = (valuePath: SingleValueType, leaf: boolean, fromKeyboard = false) => {
onSelect(valuePath);
if (!multiple && (leaf || (changeOnSelect && (expandTrigger === 'hover' || fromKeyboard)))) {
toggleOpen(false);
}
};
// ========================== Option ==========================
const mergedOptions = React.useMemo(() => {
if (searchValue) {
return searchOptions;
}
return options;
}, [searchValue, searchOptions, options]);
// ========================== Column ==========================
const optionColumns = React.useMemo(() => {
const optionList = [{ options: mergedOptions }];
let currentList = mergedOptions;
for (let i = 0; i < activeValueCells.length; i += 1) {
const activeValueCell = activeValueCells[i];
const currentOption = currentList.find(
option => option[fieldNames.value] === activeValueCell,
);
const subOptions = currentOption?.[fieldNames.children];
if (!subOptions?.length) {
break;
}
currentList = subOptions;
optionList.push({ options: subOptions });
}
return optionList;
}, [mergedOptions, activeValueCells, fieldNames]);
// ========================= Keyboard =========================
const onKeyboardSelect = (selectValueCells: SingleValueType, option: DefaultOptionType) => {
if (isSelectable(option)) {
onPathSelect(selectValueCells, isLeaf(option, fieldNames), true);
}
};
useKeyboard(
ref,
mergedOptions,
fieldNames,
activeValueCells,
onPathOpen,
containerRef,
onKeyboardSelect,
);
// ========================== Render ==========================
// >>>>> Empty
const isEmpty = !optionColumns[0]?.options?.length;
const emptyList: DefaultOptionType[] = [
{
[fieldNames.label as 'label']: notFoundContent,
[fieldNames.value as 'value']: '__EMPTY__',
disabled: true,
},
];
const columnProps = {
...props,
multiple: !isEmpty && multiple,
onSelect: onPathSelect,
onActive: onPathOpen,
onToggleOpen: toggleOpen,
checkedSet,
halfCheckedSet,
loadingKeys,
isSelectable,
};
// >>>>> Columns
const mergedOptionColumns = isEmpty ? [{ options: emptyList }] : optionColumns;
const columnNodes: React.ReactElement[] = mergedOptionColumns.map((col, index) => {
const prevValuePath = activeValueCells.slice(0, index);
const activeValue = activeValueCells[index];
return (
<Column
key={index}
{...columnProps}
prefixCls={mergedPrefixCls}
options={col.options}
prevValuePath={prevValuePath}
activeValue={activeValue}
/>
);
});
// >>>>> Render
return (
<>
<div
className={classNames(`${mergedPrefixCls}-menus`, {
[`${mergedPrefixCls}-menu-empty`]: isEmpty,
[`${mergedPrefixCls}-rtl`]: rtl,
})}
ref={containerRef}
>
{columnNodes}
</div>
</>
);
});
export default RefOptionList;