From 27c23222105a5b780aaf2398c76695c8a0ee502d Mon Sep 17 00:00:00 2001 From: jin-sir <942725119@qq.com> Date: Thu, 12 Sep 2024 19:50:23 +0800 Subject: [PATCH 1/2] feat: supports uncontrolled and interactive optimization --- src/dropdown/__tests__/dropdown.test.tsx | 35 +++++++-- src/dropdown/demos/submit.tsx | 6 +- src/dropdown/demos/virtual.tsx | 3 - src/dropdown/index.md | 2 +- src/dropdown/select.tsx | 91 +++++++++++++----------- 5 files changed, 83 insertions(+), 54 deletions(-) diff --git a/src/dropdown/__tests__/dropdown.test.tsx b/src/dropdown/__tests__/dropdown.test.tsx index 5188a8f55..9192b3aec 100644 --- a/src/dropdown/__tests__/dropdown.test.tsx +++ b/src/dropdown/__tests__/dropdown.test.tsx @@ -34,6 +34,25 @@ describe('Test Dropdown.Select Component', () => { expect(asFragment()).toMatchSnapshot(); }); + it('Should support defaultValue', () => { + const fn = jest.fn(); + const { getByTestId } = render( + idx)} + onChange={fn} + getPopupContainer={(node) => node} + > + + + ); + fireEvent.click(getByTestId('trigger')); + fireEvent.click(getByTestId('trigger')); + expect(fn).toBeCalledWith([2, 3]); + }); + it('Should enable virtual list', () => { const { container, getByTestId } = render( { }); // 全选 fireEvent.click(getByText('全选')); + fireEvent.click(getByText('确 定')); expect(fn).toBeCalledWith([1, 2]); rerender( @@ -163,6 +183,7 @@ describe('Test Dropdown.Select Component', () => { jest.runAllTimers(); }); fireEvent.click(getByText('全选')); + fireEvent.click(getByText('确 定')); // 取消全选不会取消禁用项的选择 expect(fn).lastCalledWith([2]); @@ -185,6 +206,7 @@ describe('Test Dropdown.Select Component', () => { }); // 选中全部 fireEvent.click(getByText('全选')); + fireEvent.click(getByText('确 定')); expect(fn).lastCalledWith(['Bob', 'Jack']); }); @@ -228,7 +250,7 @@ describe('Test Dropdown.Select Component', () => { expect(shadow?.className).not.toContain('active'); }); - it('Should call submit when hide', () => { + it('Should call change when hide', () => { const fn = jest.fn(); const { getByTestId, getByText } = render( { { label: '选项一', value: 1 }, { label: '选项二', value: 2, disabled: true }, ]} - onChange={jest.fn()} - onSubmit={fn} + onChange={fn} getPopupContainer={(node) => node} > diff --git a/src/dropdown/demos/virtual.tsx b/src/dropdown/demos/virtual.tsx index 9c49d45bb..55e688141 100644 --- a/src/dropdown/demos/virtual.tsx +++ b/src/dropdown/demos/virtual.tsx @@ -13,9 +13,6 @@ export default () => { console.log(val); setSelected(val as number[]); }} - onSubmit={() => { - console.log('submit'); - }} > diff --git a/src/dropdown/index.md b/src/dropdown/index.md index 9bfb21f97..fb32d4bb5 100644 --- a/src/dropdown/index.md +++ b/src/dropdown/index.md @@ -23,8 +23,8 @@ demo: | 参数 | 说明 | 类型 | 默认值 | | ----------------- | ---------------------------------- | ------------------------------------------- | ------ | | value | 当前选中的值 | `(string \| number)[]` | - | +| defaultValue | 初始值 | `(string \| number)[]` | - | | className | - | `string` | - | | options | Checkbox 指定可选项 | `(string \| number \| Option)[]` | `[]` | | getPopupContainer | 同 Dropdown 的 `getPopupContainer` | `(triggerNode: HTMLElement) => HTMLElement` | - | | onChange | 变化时的回调函数 | `(checkedValue) => void` | - | -| onSubmit | 提交时的回调函数 | `(checkedValue) => void` | - | diff --git a/src/dropdown/select.tsx b/src/dropdown/select.tsx index b0b6323a1..b5e072e20 100644 --- a/src/dropdown/select.tsx +++ b/src/dropdown/select.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useState } from 'react'; +import React, { ReactNode, useEffect, useMemo, useState } from 'react'; import { Button, Checkbox, Col, Dropdown, type DropDownProps, Row, Space } from 'antd'; import type { CheckboxChangeEvent } from 'antd/lib/checkbox'; import type { CheckboxGroupProps, CheckboxValueType } from 'antd/lib/checkbox/Group'; @@ -10,10 +10,9 @@ import './style.scss'; interface IDropdownSelectProps extends Pick, - Required> { + Pick { children: ReactNode; className?: string; - onSubmit?: (value: CheckboxValueType[]) => void; } const prefix = 'dtc-dropdown-select'; @@ -23,37 +22,37 @@ const MAX_HEIGHT = 264; export default function Select({ className, value, + defaultValue, options: rawOptions, children, getPopupContainer, onChange, - onSubmit, }: IDropdownSelectProps) { const [visible, setVisible] = useState(false); + const [selected, setSelected] = useState(defaultValue || []); const handleCheckedAll = (e: CheckboxChangeEvent) => { if (e.target.checked) { - onChange?.(options?.map((i) => i.value) || []); + setSelected(options?.map((i) => i.value) || []); } else { handleReset(); } }; const handleSubmit = () => { - onSubmit?.(value); + onChange?.(selected); setVisible(false); }; const handleReset = () => { // Clear checked but disabled item - onChange?.(options?.filter((i) => i.disabled).map((i) => i.value) || []); + setSelected(disabledValue); }; const handleChange = (e: CheckboxChangeEvent) => { - const next = e.target.checked - ? [...(value || []), e.target.value] - : value?.filter((i) => i !== e.target.value); - onChange?.(next); + const { checked, value } = e.target; + const next = checked ? [...selected, value] : selected?.filter((i) => i !== value); + setSelected(next); }; const handleShadow = (target: HTMLDivElement) => { @@ -64,51 +63,59 @@ export default function Select({ target.insertBefore(shadow, target.firstChild); } - if ( - Number( - target - .querySelector('.rc-virtual-list-scrollbar-thumb') - ?.style.top.replace('px', '') - ) > 0 - ) { - target.querySelector(`.${prefix}__shadow`)?.classList.add('active'); + const scrollbar_thumb = target.querySelector( + '.rc-virtual-list-scrollbar-thumb' + ); + const shadow = target.querySelector(`.${prefix}__shadow`); + + if (parseFloat(scrollbar_thumb?.style.top as string) > 0) { + shadow?.classList.add('active'); } else { - target - .querySelector(`.${prefix}__shadow`) - ?.classList.remove('active'); + shadow?.classList.remove('active'); } } }; - // Always turn string and number options into complex options - const options = rawOptions.map((i) => { - if (typeof i === 'string' || typeof i === 'number') { - return { - label: i, - value: i, - }; + useEffect(() => { + if (value && value !== selected) { + setSelected(value || []); } + }, [value]); + + // Always turn string and number options into complex options + const options = useMemo(() => { + return ( + rawOptions?.map((i) => { + if (typeof i === 'string' || typeof i === 'number') { + return { + label: i, + value: i, + }; + } - return i; - }); + return i; + }) || [] + ); + }, [rawOptions]); - const resetDisabled = value.every((i) => - options - ?.filter((i) => i.disabled) - .map((i) => i.value) - ?.includes(i) - ); + const disabledValue = useMemo(() => { + return options?.filter((i) => i.disabled).map((i) => i.value) || []; + }, [options]); + + const resetDisabled = selected.every((i) => disabledValue?.includes(i)); // If options' number is larger then the maxHeight, then enable virtual list const virtual = options.length > Math.floor(MAX_HEIGHT / ITEM_HEIGHT); // ONLY the options are all be pushed into value array means select all - const checkAll = !!value?.length && isEqual(options.map((i) => i.value).sort(), value.sort()); + const checkAll = + !!selected?.length && isEqual(options.map((i) => i.value).sort(), [...selected].sort()); + // At least one option's value is included in value array but not all options means indeterminate select const indeterminate = - !!value?.length && - !isEqual(options.map((i) => i.value).sort(), value.sort()) && - options.some((o) => value.includes(o.value)); + !!selected?.length && + !isEqual(options.map((i) => i.value).sort(), [...selected].sort()) && + options.some((o) => selected.includes(o.value)); const overlay = ( <> @@ -123,7 +130,7 @@ export default function Select({ - + Date: Wed, 25 Sep 2024 20:26:51 +0800 Subject: [PATCH 2/2] fix: value priority is greater than defaultValue --- src/dropdown/select.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/dropdown/select.tsx b/src/dropdown/select.tsx index b5e072e20..86c41ba0a 100644 --- a/src/dropdown/select.tsx +++ b/src/dropdown/select.tsx @@ -1,7 +1,11 @@ import React, { ReactNode, useEffect, useMemo, useState } from 'react'; import { Button, Checkbox, Col, Dropdown, type DropDownProps, Row, Space } from 'antd'; import type { CheckboxChangeEvent } from 'antd/lib/checkbox'; -import type { CheckboxGroupProps, CheckboxValueType } from 'antd/lib/checkbox/Group'; +import type { + CheckboxGroupProps, + CheckboxOptionType, + CheckboxValueType, +} from 'antd/lib/checkbox/Group'; import classNames from 'classnames'; import { isEqual } from 'lodash'; import List from 'rc-virtual-list'; @@ -29,7 +33,7 @@ export default function Select({ onChange, }: IDropdownSelectProps) { const [visible, setVisible] = useState(false); - const [selected, setSelected] = useState(defaultValue || []); + const [selected, setSelected] = useState(value || defaultValue || []); const handleCheckedAll = (e: CheckboxChangeEvent) => { if (e.target.checked) { @@ -77,13 +81,13 @@ export default function Select({ }; useEffect(() => { - if (value && value !== selected) { + if (value !== undefined && value !== selected) { setSelected(value || []); } }, [value]); // Always turn string and number options into complex options - const options = useMemo(() => { + const options = useMemo(() => { return ( rawOptions?.map((i) => { if (typeof i === 'string' || typeof i === 'number') { @@ -98,7 +102,7 @@ export default function Select({ ); }, [rawOptions]); - const disabledValue = useMemo(() => { + const disabledValue = useMemo(() => { return options?.filter((i) => i.disabled).map((i) => i.value) || []; }, [options]);