;
height: number;
itemHeight: number;
@@ -59,6 +63,7 @@ const OptionList: React.RefForwardingComponent<
{
prefixCls,
id,
+ fieldNames,
flattenOptions,
childrenAsData,
values,
@@ -246,7 +251,9 @@ const OptionList: React.RefForwardingComponent<
);
}
- function renderItem(index: number) {
+ const omitFieldNameList = Object.values(fillFieldNames(fieldNames));
+
+ const renderItem = (index: number) => {
const item = memoFlattenOptions[index];
if (!item) return null;
@@ -266,7 +273,7 @@ const OptionList: React.RefForwardingComponent<
{value}
) : null;
- }
+ };
return (
<>
@@ -287,8 +294,8 @@ const OptionList: React.RefForwardingComponent<
virtual={virtual}
onMouseEnter={onMouseEnter}
>
- {({ group, groupOption, data }, itemIndex) => {
- const { label, key } = data;
+ {({ group, groupOption, data, label, value }, itemIndex) => {
+ const { key } = data;
// Group
if (group) {
@@ -299,15 +306,8 @@ const OptionList: React.RefForwardingComponent<
);
}
- const {
- disabled,
- value,
- title,
- children,
- style,
- className,
- ...otherProps
- } = data as OptionData;
+ const { disabled, title, children, style, className, ...otherProps } = data as OptionData;
+ const passedProps = omit(otherProps, omitFieldNameList);
// Option
const selected = values.has(value);
@@ -337,7 +337,7 @@ const OptionList: React.RefForwardingComponent<
return (
extends Re
/** Config max length of input. This is only work when `mode` is `combobox` */
maxLength?: number;
+ // Field
+ fieldNames?: FieldNames;
+
// Search
inputValue?: string;
searchValue?: string;
@@ -276,6 +279,8 @@ export default function generateSelector<
autoClearSearchValue = true,
onSearch,
+ fieldNames,
+
// Icons
allowClear,
clearIcon,
@@ -961,6 +966,7 @@ export default function generateSelector<
open={mergedOpen}
childrenAsData={!options}
options={displayOptions}
+ fieldNames={fieldNames}
flattenOptions={displayFlattenOptions}
multiple={isMultiple}
values={rawValues}
diff --git a/src/hooks/useCacheOptions.ts b/src/hooks/useCacheOptions.ts
index fb1088086..06aef44c8 100644
--- a/src/hooks/useCacheOptions.ts
+++ b/src/hooks/useCacheOptions.ts
@@ -16,9 +16,7 @@ export default function useCacheOptions<
const optionMap = React.useMemo(() => {
const map: Map[number]> = new Map();
options.forEach((item) => {
- const {
- data: { value },
- } = item;
+ const { value } = item;
map.set(value, item);
});
return map;
@@ -26,8 +24,8 @@ export default function useCacheOptions<
prevOptionMapRef.current = optionMap;
- const getValueOption = (vals: RawValueType[]): FlattenOptionsType =>
- vals.map((value) => prevOptionMapRef.current.get(value)).filter(Boolean);
+ const getValueOption = (valueList: RawValueType[]): FlattenOptionsType =>
+ valueList.map((value) => prevOptionMapRef.current.get(value)).filter(Boolean);
return getValueOption;
}
diff --git a/src/interface/generator.ts b/src/interface/generator.ts
index 7b78c0a11..56c90f0f5 100644
--- a/src/interface/generator.ts
+++ b/src/interface/generator.ts
@@ -65,6 +65,8 @@ export declare function RefSelectFunc(
export type FlattenOptionsType = {
key: Key;
data: OptionsType[number];
+ label?: React.ReactNode;
+ value?: RawValueType;
/** Used for customize data */
[name: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
}[];
diff --git a/src/interface/index.ts b/src/interface/index.ts
index 106cf09e8..a84b4a7e5 100644
--- a/src/interface/index.ts
+++ b/src/interface/index.ts
@@ -8,6 +8,12 @@ export type RenderNode = React.ReactNode | ((props: any) => React.ReactNode);
export type Mode = 'multiple' | 'tags' | 'combobox';
// ======================== Option ========================
+export interface FieldNames {
+ value?: string;
+ label?: string;
+ options?: string;
+}
+
export type OnActiveValue = (
active: RawValueType,
index: number,
@@ -49,4 +55,6 @@ export interface FlattenOptionData {
groupOption?: boolean;
key: string | number;
data: OptionData | OptionGroupData;
+ label?: React.ReactNode;
+ value?: React.Key;
}
diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts
index 1622c344e..fcb319813 100644
--- a/src/utils/valueUtil.ts
+++ b/src/utils/valueUtil.ts
@@ -4,6 +4,7 @@ import type {
OptionData,
OptionGroupData,
FlattenOptionData,
+ FieldNames,
} from '../interface';
import type {
LabelValueType,
@@ -32,22 +33,45 @@ function getKey(data: OptionData | OptionGroupData, index: number) {
return `rc-index-key-${index}`;
}
+export function fillFieldNames(fieldNames?: FieldNames) {
+ const { label, value, options } = fieldNames || {};
+
+ return {
+ label: label || 'label',
+ value: value || 'value',
+ options: options || 'options',
+ };
+}
+
/**
* Flat options into flatten list.
* We use `optionOnly` here is aim to avoid user use nested option group.
* Here is simply set `key` to the index if not provided.
*/
-export function flattenOptions(options: SelectOptionsType): FlattenOptionData[] {
+export function flattenOptions(
+ options: SelectOptionsType,
+ { fieldNames }: { fieldNames?: FieldNames } = {},
+): FlattenOptionData[] {
const flattenList: FlattenOptionData[] = [];
+ const {
+ label: fieldLabel,
+ value: fieldValue,
+ options: fieldOptions,
+ } = fillFieldNames(fieldNames);
+
function dig(list: SelectOptionsType, isGroupOption: boolean) {
list.forEach((data) => {
- if (isGroupOption || !('options' in data)) {
+ const label = data[fieldLabel];
+
+ if (isGroupOption || !(fieldOptions in data)) {
// Option
flattenList.push({
key: getKey(data, flattenList.length),
groupOption: isGroupOption,
data,
+ label,
+ value: data[fieldValue],
});
} else {
// Option Group
@@ -55,9 +79,10 @@ export function flattenOptions(options: SelectOptionsType): FlattenOptionData[]
key: getKey(data, flattenList.length),
group: true,
data,
+ label,
});
- dig(data.options, true);
+ dig(data[fieldOptions], true);
}
});
}
@@ -94,11 +119,10 @@ export function findValueOption(
): OptionData[] {
const optionMap: Map = new Map();
- options.forEach((flattenItem) => {
- if (!flattenItem.group) {
- const data = flattenItem.data as OptionData;
+ options.forEach(({ data, group, value }) => {
+ if (!group) {
// Check if match
- optionMap.set(data.value, data);
+ optionMap.set(value, data as OptionData);
}
});
@@ -120,7 +144,7 @@ export function findValueOption(
export const getLabeledValue: GetLabeledValue = (
value,
{ options, prevValueMap, labelInValue, optionLabelProp },
-) => {
+): LabelValueType => {
const item = findValueOption([value], options)[0];
const result: LabelValueType = {
value,
diff --git a/tests/Field.test.tsx b/tests/Field.test.tsx
new file mode 100644
index 000000000..6a918ceba
--- /dev/null
+++ b/tests/Field.test.tsx
@@ -0,0 +1,79 @@
+/* eslint-disable import/no-named-as-default-member */
+import { mount } from 'enzyme';
+import { act } from 'react-dom/test-utils';
+import * as React from 'react';
+import Select from '../src';
+import type { SelectProps } from '../src';
+import { injectRunAllTimers } from './utils/common';
+
+describe('Select.Field', () => {
+ injectRunAllTimers(jest);
+
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ const OPTION_1 = { bambooLabel: 'Light', bambooValue: 'light' };
+ const OPTION_2 = { bambooLabel: 'Little', bambooValue: 'little' };
+
+ function mountSelect(props?: Partial>) {
+ return mount(
+ ,
+ );
+ }
+
+ it('fieldNames should work', () => {
+ const onChange = jest.fn();
+ const onSelect = jest.fn();
+
+ const wrapper = mountSelect({ onChange, onSelect });
+
+ act(() => {
+ jest.runAllTimers();
+ });
+
+ // Label match
+ expect(wrapper.find('.rc-select-item-group').text()).toEqual('Bamboo');
+ expect(wrapper.find('.rc-select-item-option').first().text()).toEqual('Light');
+ expect(wrapper.find('.rc-select-item-option').last().text()).toEqual('Little');
+
+ // Click
+ wrapper.find('.rc-select-item-option-content').last().simulate('click');
+ expect(onChange).toHaveBeenCalledWith('little', OPTION_2);
+ expect(onSelect).toHaveBeenCalledWith('little', OPTION_2);
+ });
+
+ it('multiple', () => {
+ const onChange = jest.fn();
+ const wrapper = mountSelect({ mode: 'multiple', onChange });
+
+ // First one
+ wrapper.find('.rc-select-item-option-content').first().simulate('click');
+ expect(onChange).toHaveBeenCalledWith(['light'], [OPTION_1]);
+
+ // Last one
+ onChange.mockReset();
+ wrapper.find('.rc-select-item-option-content').last().simulate('click');
+ expect(onChange).toHaveBeenCalledWith(['light', 'little'], [OPTION_1, OPTION_2]);
+ });
+});