From 756c3945a437a4f6b1c8655079b2a08e632e5e33 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 23 Jun 2021 18:05:25 +0800 Subject: [PATCH 1/4] chore: Init fieldNames --- src/OptionList.tsx | 28 +++++++++++------------ src/generate.tsx | 8 ++++++- src/interface/index.ts | 8 +++++++ src/utils/valueUtil.ts | 24 ++++++++++++++++++-- tests/Field.test.tsx | 50 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 17 deletions(-) create mode 100644 tests/Field.test.tsx diff --git a/src/OptionList.tsx b/src/OptionList.tsx index 3b175d181..aa4b71aa0 100644 --- a/src/OptionList.tsx +++ b/src/OptionList.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import KeyCode from 'rc-util/lib/KeyCode'; +import omit from 'rc-util/lib/omit'; import pickAttrs from 'rc-util/lib/pickAttrs'; import useMemo from 'rc-util/lib/hooks/useMemo'; import classNames from 'classnames'; @@ -12,13 +13,16 @@ import type { OptionData, RenderNode, OnActiveValue, + FieldNames, } from './interface'; import type { RawValueType, FlattenOptionsType } from './interface/generator'; +import { fillFieldNames } from './utils/valueUtil'; export interface OptionListProps { prefixCls: string; id: string; options: OptionsType; + fieldNames?: FieldNames; flattenOptions: FlattenOptionsType; 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.keys(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/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..20e924ad9 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,14 +33,31 @@ 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)) { @@ -55,9 +73,11 @@ export function flattenOptions(options: SelectOptionsType): FlattenOptionData[] key: getKey(data, flattenList.length), group: true, data, + label: data[fieldLabel], + value: data[fieldValue], }); - dig(data.options, true); + dig(data[fieldOptions], true); } }); } diff --git a/tests/Field.test.tsx b/tests/Field.test.tsx new file mode 100644 index 000000000..a802717c0 --- /dev/null +++ b/tests/Field.test.tsx @@ -0,0 +1,50 @@ +/* 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 { injectRunAllTimers } from './utils/common'; + +describe('Select.Field', () => { + injectRunAllTimers(jest); + + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('fieldNames should work', () => { + // Use special name to avoid compatible match + const wrapper = mount( + + ); +}; +/* eslint-enable */ 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/utils/valueUtil.ts b/src/utils/valueUtil.ts index 487e45c72..195643eb7 100644 --- a/src/utils/valueUtil.ts +++ b/src/utils/valueUtil.ts @@ -143,7 +143,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 index a802717c0..437b64440 100644 --- a/tests/Field.test.tsx +++ b/tests/Field.test.tsx @@ -17,10 +17,12 @@ describe('Select.Field', () => { }); it('fieldNames should work', () => { - // Use special name to avoid compatible match + const onChange = jest.fn(); + const wrapper = mount( { value: 'bambooValue', options: 'bambooChildren', }} + {...props} />, ); + } + + it('fieldNames should work', () => { + const onChange = jest.fn(); + const onSelect = jest.fn(); + + const wrapper = mountSelect({ onChange, onSelect }); act(() => { jest.runAllTimers(); @@ -53,6 +59,21 @@ describe('Select.Field', () => { // Click wrapper.find('.rc-select-item-option-content').last().simulate('click'); - expect(onChange).toHaveBeenCalledWith(2333); + 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]); }); });