1
This commit is contained in:
+10
@@ -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;
|
||||
+24
@@ -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);
|
||||
}
|
||||
+21
@@ -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;
|
||||
+199
@@ -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));
|
||||
}));
|
||||
}
|
||||
+10
@@ -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;
|
||||
+217
@@ -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;
|
||||
+4
@@ -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;
|
||||
+17
@@ -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;
|
||||
+6
@@ -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;
|
||||
+24
@@ -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;
|
||||
Generated
Vendored
+10
@@ -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;
|
||||
+165
@@ -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: () => {}
|
||||
}));
|
||||
});
|
||||
Reference in New Issue
Block a user