This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
@@ -0,0 +1,10 @@
import * as React from 'react';
export interface CheckboxProps {
prefixCls: string;
checked?: boolean;
halfChecked?: boolean;
disabled?: boolean;
onClick?: React.MouseEventHandler;
disableCheckbox?: boolean;
}
export default function Checkbox({ prefixCls, checked, halfChecked, disabled, onClick, disableCheckbox, }: CheckboxProps): React.JSX.Element;
@@ -0,0 +1,24 @@
import * as React from 'react';
import { clsx } from 'clsx';
import CascaderContext from "../context";
export default function Checkbox({
prefixCls,
checked,
halfChecked,
disabled,
onClick,
disableCheckbox
}) {
const {
checkable
} = React.useContext(CascaderContext);
const customCheckbox = typeof checkable !== 'boolean' ? checkable : null;
return /*#__PURE__*/React.createElement("span", {
className: clsx(`${prefixCls}`, {
[`${prefixCls}-checked`]: checked,
[`${prefixCls}-indeterminate`]: !checked && halfChecked,
[`${prefixCls}-disabled`]: disabled || disableCheckbox
}),
onClick: onClick
}, customCheckbox);
}
@@ -0,0 +1,21 @@
import * as React from 'react';
import type { DefaultOptionType, SingleValueType } from '../Cascader';
export declare const FIX_LABEL = "__cascader_fix_label__";
export interface ColumnProps<OptionType extends DefaultOptionType = DefaultOptionType> {
prefixCls: string;
multiple?: boolean;
options: OptionType[];
/** Current Column opened item key */
activeValue?: React.Key;
/** The value path before current column */
prevValuePath: React.Key[];
onToggleOpen: (open: boolean) => void;
onSelect: (valuePath: SingleValueType, leaf: boolean) => void;
onActive: (valuePath: SingleValueType) => void;
checkedSet: Set<React.Key>;
halfCheckedSet: Set<React.Key>;
loadingKeys: React.Key[];
isSelectable: (option: DefaultOptionType) => boolean;
disabled?: boolean;
}
export default function Column<OptionType extends DefaultOptionType = DefaultOptionType>({ prefixCls, multiple, options, activeValue, prevValuePath, onToggleOpen, onSelect, onActive, checkedSet, halfCheckedSet, loadingKeys, isSelectable, disabled: propsDisabled, }: ColumnProps<OptionType>): React.JSX.Element;
@@ -0,0 +1,199 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import { clsx } from 'clsx';
import * as React from 'react';
import pickAttrs from "@rc-component/util/es/pickAttrs";
import CascaderContext from "../context";
import { SEARCH_MARK } from "../hooks/useSearchOptions";
import { isLeaf, scrollIntoParentView, toPathKey } from "../utils/commonUtil";
import Checkbox from "./Checkbox";
export const FIX_LABEL = '__cascader_fix_label__';
export default function Column({
prefixCls,
multiple,
options,
activeValue,
prevValuePath,
onToggleOpen,
onSelect,
onActive,
checkedSet,
halfCheckedSet,
loadingKeys,
isSelectable,
disabled: propsDisabled
}) {
const menuPrefixCls = `${prefixCls}-menu`;
const menuItemPrefixCls = `${prefixCls}-menu-item`;
const menuRef = React.useRef(null);
const {
fieldNames,
changeOnSelect,
expandTrigger,
expandIcon,
loadingIcon,
popupMenuColumnStyle,
optionRender,
classNames,
styles
} = React.useContext(CascaderContext);
const hoverOpen = expandTrigger === 'hover';
const isOptionDisabled = disabled => propsDisabled || disabled;
// ============================ Option ============================
const optionInfoList = React.useMemo(() => options.map(option => {
const {
disabled,
disableCheckbox
} = option;
const searchOptions = option[SEARCH_MARK];
const label = option[FIX_LABEL] ?? option[fieldNames.label];
const value = option[fieldNames.value];
const isMergedLeaf = isLeaf(option, fieldNames);
// Get real value of option. Search option is different way.
const fullPath = searchOptions ? searchOptions.map(opt => opt[fieldNames.value]) : [...prevValuePath, value];
const fullPathKey = toPathKey(fullPath);
const isLoading = loadingKeys.includes(fullPathKey);
// >>>>> checked
const checked = checkedSet.has(fullPathKey);
// >>>>> halfChecked
const halfChecked = halfCheckedSet.has(fullPathKey);
return {
disabled,
label,
value,
isLeaf: isMergedLeaf,
isLoading,
checked,
halfChecked,
option,
disableCheckbox,
fullPath,
fullPathKey
};
}), [options, checkedSet, fieldNames, halfCheckedSet, loadingKeys, prevValuePath]);
React.useEffect(() => {
if (menuRef.current) {
const selector = `.${menuItemPrefixCls}-active`;
const activeElement = menuRef.current.querySelector(selector);
if (activeElement) {
scrollIntoParentView(activeElement);
}
}
}, [activeValue, menuItemPrefixCls]);
// ============================ Render ============================
return /*#__PURE__*/React.createElement("ul", {
className: clsx(menuPrefixCls, classNames?.popup?.list),
style: styles?.popup?.list,
ref: menuRef,
role: "menu"
}, optionInfoList.map(({
disabled,
label,
value,
isLeaf: isMergedLeaf,
isLoading,
checked,
halfChecked,
option,
fullPath,
fullPathKey,
disableCheckbox
}) => {
const ariaProps = pickAttrs(option, {
aria: true,
data: true
});
// >>>>> Open
const triggerOpenPath = () => {
if (isOptionDisabled(disabled)) {
return;
}
const nextValueCells = [...fullPath];
if (hoverOpen && isMergedLeaf) {
nextValueCells.pop();
}
onActive(nextValueCells);
};
// >>>>> Selection
const triggerSelect = () => {
if (isSelectable(option) && !isOptionDisabled(disabled)) {
onSelect(fullPath, isMergedLeaf);
}
};
// >>>>> Title
let title;
if (typeof option.title === 'string') {
title = option.title;
} else if (typeof label === 'string') {
title = label;
}
// >>>>> Render
return /*#__PURE__*/React.createElement("li", _extends({
key: fullPathKey
}, ariaProps, {
className: clsx(menuItemPrefixCls, classNames?.popup?.listItem, {
[`${menuItemPrefixCls}-expand`]: !isMergedLeaf,
[`${menuItemPrefixCls}-active`]: activeValue === value || activeValue === fullPathKey,
[`${menuItemPrefixCls}-disabled`]: isOptionDisabled(disabled),
[`${menuItemPrefixCls}-loading`]: isLoading
}),
style: {
...popupMenuColumnStyle,
...styles?.popup?.listItem
},
role: "menuitemcheckbox",
title: title,
"aria-checked": checked,
"data-path-key": fullPathKey,
onClick: () => {
triggerOpenPath();
if (disableCheckbox) {
return;
}
if (!multiple || isMergedLeaf) {
triggerSelect();
}
},
onDoubleClick: () => {
if (changeOnSelect) {
onToggleOpen(false);
}
},
onMouseEnter: () => {
if (hoverOpen) {
triggerOpenPath();
}
},
onMouseDown: e => {
// Prevent selector from blurring
e.preventDefault();
}
}), multiple && /*#__PURE__*/React.createElement(Checkbox, {
prefixCls: `${prefixCls}-checkbox`,
checked: checked,
halfChecked: halfChecked,
disabled: isOptionDisabled(disabled) || disableCheckbox,
disableCheckbox: disableCheckbox,
onClick: e => {
if (disableCheckbox) {
return;
}
e.stopPropagation();
triggerSelect();
}
}), /*#__PURE__*/React.createElement("div", {
className: `${menuItemPrefixCls}-content`
}, optionRender && value !== '__EMPTY__' ? optionRender(option) : label), !isLoading && expandIcon && !isMergedLeaf && /*#__PURE__*/React.createElement("div", {
className: `${menuItemPrefixCls}-expand-icon`
}, expandIcon), isLoading && loadingIcon && /*#__PURE__*/React.createElement("div", {
className: `${menuItemPrefixCls}-loading-icon`
}, loadingIcon));
}));
}
@@ -0,0 +1,10 @@
import type { useBaseProps } from '@rc-component/select';
import type { RefOptionListProps } from '@rc-component/select/lib/OptionList';
import * as React from 'react';
export type RawOptionListProps = Pick<ReturnType<typeof useBaseProps>, 'prefixCls' | 'multiple' | 'searchValue' | 'toggleOpen' | 'notFoundContent' | 'direction' | 'open' | 'disabled'> & {
lockOptions?: boolean;
};
declare const RawOptionList: React.ForwardRefExoticComponent<Pick<import("@rc-component/select/lib/hooks/useBaseProps").BaseSelectContextProps, "disabled" | "prefixCls" | "multiple" | "searchValue" | "direction" | "notFoundContent" | "open" | "toggleOpen"> & {
lockOptions?: boolean | undefined;
} & React.RefAttributes<RefOptionListProps>>;
export default RawOptionList;
@@ -0,0 +1,217 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
/* eslint-disable default-case */
import { clsx } from 'clsx';
import * as React from 'react';
import useMemo from "@rc-component/util/es/hooks/useMemo";
import CascaderContext from "../context";
import { getFullPathKeys, isLeaf, scrollIntoParentView, toPathKey, toPathKeys, toPathValueStr } from "../utils/commonUtil";
import { toPathOptions } from "../utils/treeUtil";
import Column, { FIX_LABEL } from "./Column";
import useActive from "./useActive";
import useKeyboard from "./useKeyboard";
const RawOptionList = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
prefixCls,
multiple,
searchValue,
toggleOpen,
notFoundContent,
direction,
open,
disabled,
lockOptions = false
} = props;
const containerRef = React.useRef(null);
const rtl = direction === 'rtl';
const {
options,
values,
halfValues,
fieldNames,
changeOnSelect,
onSelect,
searchOptions,
popupPrefixCls,
loadData,
expandTrigger
} = React.useContext(CascaderContext);
const mergedPrefixCls = popupPrefixCls || prefixCls;
// ========================= loadData =========================
const [loadingKeys, setLoadingKeys] = React.useState([]);
const internalLoadData = valueCells => {
// 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(multiple, open);
// =========================== Path ===========================
const onPathOpen = nextValueCells => {
setActiveValueCells(nextValueCells);
// Trigger loadData
internalLoadData(nextValueCells);
};
const isSelectable = option => {
if (disabled) {
return false;
}
const {
disabled: optionDisabled
} = option;
const isMergedLeaf = isLeaf(option, fieldNames);
return !optionDisabled && (isMergedLeaf || changeOnSelect || multiple);
};
const onPathSelect = (valuePath, leaf, fromKeyboard = false) => {
onSelect(valuePath);
if (!multiple && (leaf || changeOnSelect && (expandTrigger === 'hover' || fromKeyboard))) {
toggleOpen(false);
}
};
// ========================== Option ==========================
const filteredOptions = React.useMemo(() => {
if (searchValue) {
return searchOptions;
}
return options;
}, [searchValue, searchOptions, options]);
// Update only when open or lockOptions
const mergedOptions = useMemo(() => filteredOptions, [open, lockOptions], (prev, next) => !!next[0] && !next[1]);
// ========================== Column ==========================
const optionColumns = React.useMemo(() => {
const optionList = [{
options: mergedOptions
}];
let currentList = mergedOptions;
const fullPathKeys = getFullPathKeys(currentList, fieldNames);
for (let i = 0; i < activeValueCells.length; i += 1) {
const activeValueCell = activeValueCells[i];
const currentOption = currentList.find((option, index) => (fullPathKeys[index] ? toPathKey(fullPathKeys[index]) : 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, option) => {
if (isSelectable(option)) {
onPathSelect(selectValueCells, isLeaf(option, fieldNames), true);
}
};
useKeyboard(ref, mergedOptions, fieldNames, activeValueCells, onPathOpen, onKeyboardSelect, {
direction,
searchValue,
toggleOpen,
open
});
// >>>>> Active Scroll
React.useEffect(() => {
if (searchValue) {
return;
}
for (let i = 0; i < activeValueCells.length; i += 1) {
const cellPath = activeValueCells.slice(0, i + 1);
const cellKeyPath = toPathKey(cellPath);
const ele = containerRef.current?.querySelector(`li[data-path-key="${cellKeyPath.replace(/\\{0,2}"/g, '\\"')}"]` // matches unescaped double quotes
);
if (ele) {
scrollIntoParentView(ele);
}
}
}, [activeValueCells, searchValue]);
// ========================== Render ==========================
// >>>>> Empty
const isEmpty = !optionColumns[0]?.options?.length;
const emptyList = [{
[fieldNames.value]: '__EMPTY__',
[FIX_LABEL]: notFoundContent,
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 = mergedOptionColumns.map((col, index) => {
const prevValuePath = activeValueCells.slice(0, index);
const activeValue = activeValueCells[index];
return /*#__PURE__*/React.createElement(Column, _extends({
key: index
}, columnProps, {
prefixCls: mergedPrefixCls,
options: col.options,
prevValuePath: prevValuePath,
activeValue: activeValue
}));
});
// >>>>> Render
return /*#__PURE__*/React.createElement("div", {
className: clsx(`${mergedPrefixCls}-menus`, {
[`${mergedPrefixCls}-menu-empty`]: isEmpty,
[`${mergedPrefixCls}-rtl`]: rtl
}),
ref: containerRef
}, columnNodes);
});
if (process.env.NODE_ENV !== 'production') {
RawOptionList.displayName = 'RawOptionList';
}
export default RawOptionList;
@@ -0,0 +1,4 @@
import type { RefOptionListProps } from '@rc-component/select/lib/OptionList';
import * as React from 'react';
declare const RefOptionList: React.ForwardRefExoticComponent<React.RefAttributes<RefOptionListProps>>;
export default RefOptionList;
@@ -0,0 +1,17 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import { useBaseProps } from '@rc-component/select';
import * as React from 'react';
import RawOptionList from "./List";
const RefOptionList = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
lockOptions,
...baseProps
} = useBaseProps();
// >>>>> Render
return /*#__PURE__*/React.createElement(RawOptionList, _extends({}, props, baseProps, {
lockOptions: lockOptions,
ref: ref
}));
});
export default RefOptionList;
@@ -0,0 +1,6 @@
import type { LegacyKey } from '../Cascader';
/**
* Control the active open options path.
*/
declare const useActive: (multiple?: boolean, open?: boolean) => [LegacyKey[], (activeValueCells: LegacyKey[]) => void];
export default useActive;
@@ -0,0 +1,24 @@
import * as React from 'react';
import CascaderContext from "../context";
/**
* Control the active open options path.
*/
const useActive = (multiple, open) => {
const {
values
} = React.useContext(CascaderContext);
const firstValueCells = values[0];
// Record current dropdown active options
// This also control the open status
const [activeValueCells, setActiveValueCells] = React.useState([]);
React.useEffect(() => {
if (!multiple) {
setActiveValueCells(firstValueCells || []);
}
}, /* eslint-disable react-hooks/exhaustive-deps */
[open, firstValueCells]
/* eslint-enable react-hooks/exhaustive-deps */);
return [activeValueCells, setActiveValueCells];
};
export default useActive;
@@ -0,0 +1,10 @@
import type { RefOptionListProps } from '@rc-component/select/lib/OptionList';
import * as React from 'react';
import type { DefaultOptionType, InternalFieldNames, LegacyKey, SingleValueType } from '../Cascader';
declare const _default: (ref: React.Ref<RefOptionListProps>, options: DefaultOptionType[], fieldNames: InternalFieldNames, activeValueCells: LegacyKey[], setActiveValueCells: (activeValueCells: LegacyKey[]) => void, onKeyBoardSelect: (valueCells: SingleValueType, option: DefaultOptionType) => void, contextProps: {
direction?: "ltr" | "rtl" | undefined;
searchValue: string;
toggleOpen: (open?: boolean) => void;
open?: boolean | undefined;
}) => void;
export default _default;
@@ -0,0 +1,165 @@
import KeyCode from "@rc-component/util/es/KeyCode";
import * as React from 'react';
import { SEARCH_MARK } from "../hooks/useSearchOptions";
import { getFullPathKeys, toPathKey } from "../utils/commonUtil";
export default ((ref, options, fieldNames, activeValueCells, setActiveValueCells, onKeyBoardSelect, contextProps) => {
const {
direction,
searchValue,
toggleOpen,
open
} = contextProps;
const rtl = direction === 'rtl';
const [validActiveValueCells, lastActiveIndex, lastActiveOptions, fullPathKeys] = React.useMemo(() => {
let activeIndex = -1;
let currentOptions = options;
const mergedActiveIndexes = [];
const mergedActiveValueCells = [];
const len = activeValueCells.length;
const pathKeys = getFullPathKeys(options, fieldNames);
// Fill validate active value cells and index
for (let i = 0; i < len && currentOptions; i += 1) {
// Mark the active index for current options
const nextActiveIndex = currentOptions.findIndex((option, index) => (pathKeys[index] ? toPathKey(pathKeys[index]) : option[fieldNames.value]) === activeValueCells[i]);
if (nextActiveIndex === -1) {
break;
}
activeIndex = nextActiveIndex;
mergedActiveIndexes.push(activeIndex);
mergedActiveValueCells.push(activeValueCells[i]);
currentOptions = currentOptions[activeIndex][fieldNames.children];
}
// Fill last active options
let activeOptions = options;
for (let i = 0; i < mergedActiveIndexes.length - 1; i += 1) {
activeOptions = activeOptions[mergedActiveIndexes[i]][fieldNames.children];
}
return [mergedActiveValueCells, activeIndex, activeOptions, pathKeys];
}, [activeValueCells, fieldNames, options]);
// Update active value cells and scroll to target element
const internalSetActiveValueCells = next => {
setActiveValueCells(next);
};
// Same options offset
const offsetActiveOption = offset => {
const len = lastActiveOptions.length;
let currentIndex = lastActiveIndex;
if (currentIndex === -1 && offset < 0) {
currentIndex = len;
}
for (let i = 0; i < len; i += 1) {
currentIndex = (currentIndex + offset + len) % len;
const option = lastActiveOptions[currentIndex];
if (option && !option.disabled) {
const nextActiveCells = validActiveValueCells.slice(0, -1).concat(fullPathKeys[currentIndex] ? toPathKey(fullPathKeys[currentIndex]) : option[fieldNames.value]);
internalSetActiveValueCells(nextActiveCells);
return;
}
}
};
// Different options offset
const prevColumn = () => {
if (validActiveValueCells.length > 1) {
const nextActiveCells = validActiveValueCells.slice(0, -1);
internalSetActiveValueCells(nextActiveCells);
} else {
toggleOpen(false);
}
};
const nextColumn = () => {
const nextOptions = lastActiveOptions[lastActiveIndex]?.[fieldNames.children] || [];
const nextOption = nextOptions.find(option => !option.disabled);
if (nextOption) {
const nextActiveCells = [...validActiveValueCells, nextOption[fieldNames.value]];
internalSetActiveValueCells(nextActiveCells);
}
};
React.useImperativeHandle(ref, () => ({
// scrollTo: treeRef.current?.scrollTo,
onKeyDown: event => {
const {
which
} = event;
switch (which) {
// >>> Arrow keys
case KeyCode.UP:
case KeyCode.DOWN:
{
let offset = 0;
if (which === KeyCode.UP) {
offset = -1;
} else if (which === KeyCode.DOWN) {
offset = 1;
}
if (offset !== 0) {
offsetActiveOption(offset);
}
break;
}
case KeyCode.LEFT:
{
if (searchValue) {
break;
}
if (rtl) {
nextColumn();
} else {
prevColumn();
}
break;
}
case KeyCode.RIGHT:
{
if (searchValue) {
break;
}
if (rtl) {
prevColumn();
} else {
nextColumn();
}
break;
}
case KeyCode.BACKSPACE:
{
if (!searchValue) {
prevColumn();
}
break;
}
// >>> Select
case KeyCode.ENTER:
{
if (validActiveValueCells.length) {
const option = lastActiveOptions[lastActiveIndex];
// Search option should revert back of origin options
const originOptions = option?.[SEARCH_MARK] || [];
if (originOptions.length) {
onKeyBoardSelect(originOptions.map(opt => opt[fieldNames.value]), originOptions[originOptions.length - 1]);
} else {
onKeyBoardSelect(validActiveValueCells, lastActiveOptions[lastActiveIndex]);
}
}
break;
}
// >>> Close
case KeyCode.ESC:
{
toggleOpen(false);
if (open) {
event.stopPropagation();
}
}
}
},
onKeyUp: () => {}
}));
});