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');
- }}
>
10000 条数据
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]);