Skip to content

Commit

Permalink
feat(dropdown.select): supports uncontrolled and interactive optimiza…
Browse files Browse the repository at this point in the history
…tion (#484)

* feat: supports uncontrolled and interactive optimization

* fix: value priority is greater than defaultValue
  • Loading branch information
jin-sir authored Oct 31, 2024
1 parent 6b80b7c commit 89426ce
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 55 deletions.
35 changes: 29 additions & 6 deletions src/dropdown/__tests__/dropdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,25 @@ describe('Test Dropdown.Select Component', () => {
expect(asFragment()).toMatchSnapshot();
});

it('Should support defaultValue', () => {
const fn = jest.fn();
const { getByTestId } = render(
<Dropdown.Select
defaultValue={[2, 3]}
options={new Array(10).fill('').map((_, idx) => idx)}
onChange={fn}
getPopupContainer={(node) => node}
>
<Button type="link" data-testid="trigger">
打开下拉
</Button>
</Dropdown.Select>
);
fireEvent.click(getByTestId('trigger'));
fireEvent.click(getByTestId('trigger'));
expect(fn).toBeCalledWith([2, 3]);
});

it('Should enable virtual list', () => {
const { container, getByTestId } = render(
<Dropdown.Select
Expand Down Expand Up @@ -140,6 +159,7 @@ describe('Test Dropdown.Select Component', () => {
});
// 全选
fireEvent.click(getByText('全选'));
fireEvent.click(getByText('确 定'));
expect(fn).toBeCalledWith([1, 2]);

rerender(
Expand All @@ -163,6 +183,7 @@ describe('Test Dropdown.Select Component', () => {
jest.runAllTimers();
});
fireEvent.click(getByText('全选'));
fireEvent.click(getByText('确 定'));
// 取消全选不会取消禁用项的选择
expect(fn).lastCalledWith([2]);

Expand All @@ -185,6 +206,7 @@ describe('Test Dropdown.Select Component', () => {
});
// 选中全部
fireEvent.click(getByText('全选'));
fireEvent.click(getByText('确 定'));
expect(fn).lastCalledWith(['Bob', 'Jack']);
});

Expand Down Expand Up @@ -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(
<Dropdown.Select
Expand All @@ -237,8 +259,7 @@ describe('Test Dropdown.Select Component', () => {
{ label: '选项一', value: 1 },
{ label: '选项二', value: 2, disabled: true },
]}
onChange={jest.fn()}
onSubmit={fn}
onChange={fn}
getPopupContainer={(node) => node}
>
<Button type="link" data-testid="trigger">
Expand All @@ -252,7 +273,7 @@ describe('Test Dropdown.Select Component', () => {
jest.runAllTimers();
});

fireEvent.click(getByText('确 定').parentElement!);
fireEvent.click(getByText('确 定')!);
expect(fn).toBeCalledWith([2]);
expect(fn).toBeCalledTimes(1);

Expand Down Expand Up @@ -376,9 +397,11 @@ describe('Test Dropdown.Select Component', () => {
});

fireEvent.click(getByText('1'));
expect(fn).toBeCalledWith([1000, 2, 2000, 1]);
fireEvent.click(getByText('确 定'));
expect(fn).toBeCalledWith([2, 1000, 2000, 1]);

fireEvent.click(getByText('2'));
expect(fn).toBeCalledWith([1000, 2000]);
fireEvent.click(getByText('确 定'));
expect(fn).toBeCalledWith([1000, 2000, 1]);
});
});
6 changes: 4 additions & 2 deletions src/dropdown/demos/submit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ export default () => {
{ label: '选项一', value: 1 },
{ label: '选项二', value: 2, disabled: true },
]}
onChange={(checked) => setSelected(checked as number[])}
onSubmit={fetchData}
onChange={(checked) => {
setSelected(checked as number[]);
fetchData();
}}
>
<Button type="link">打开下拉</Button>
</Dropdown.Select>
Expand Down
3 changes: 0 additions & 3 deletions src/dropdown/demos/virtual.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ export default () => {
console.log(val);
setSelected(val as number[]);
}}
onSubmit={() => {
console.log('submit');
}}
>
<Button type="link">10000 条数据</Button>
</Dropdown.Select>
Expand Down
2 changes: 1 addition & 1 deletion src/dropdown/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | - |
97 changes: 54 additions & 43 deletions src/dropdown/select.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
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';
import type {
CheckboxGroupProps,
CheckboxOptionType,
CheckboxValueType,
} from 'antd/lib/checkbox/Group';
import classNames from 'classnames';
import { isEqual } from 'lodash';
import List from 'rc-virtual-list';
Expand All @@ -10,10 +14,9 @@ import './style.scss';

interface IDropdownSelectProps
extends Pick<DropDownProps, 'getPopupContainer'>,
Required<Pick<CheckboxGroupProps, 'value' | 'options' | 'onChange'>> {
Pick<CheckboxGroupProps, 'value' | 'defaultValue' | 'options' | 'onChange'> {
children: ReactNode;
className?: string;
onSubmit?: (value: CheckboxValueType[]) => void;
}

const prefix = 'dtc-dropdown-select';
Expand All @@ -23,37 +26,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<CheckboxValueType[]>(value || 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) => {
Expand All @@ -64,51 +67,59 @@ export default function Select({
target.insertBefore(shadow, target.firstChild);
}

if (
Number(
target
.querySelector<HTMLDivElement>('.rc-virtual-list-scrollbar-thumb')
?.style.top.replace('px', '')
) > 0
) {
target.querySelector<HTMLDivElement>(`.${prefix}__shadow`)?.classList.add('active');
const scrollbar_thumb = target.querySelector<HTMLDivElement>(
'.rc-virtual-list-scrollbar-thumb'
);
const shadow = target.querySelector<HTMLDivElement>(`.${prefix}__shadow`);

if (parseFloat(scrollbar_thumb?.style.top as string) > 0) {
shadow?.classList.add('active');
} else {
target
.querySelector<HTMLDivElement>(`.${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 !== undefined && value !== selected) {
setSelected(value || []);
}
}, [value]);

// Always turn string and number options into complex options
const options = useMemo<CheckboxOptionType[]>(() => {
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<CheckboxValueType[]>(() => {
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 = (
<>
Expand All @@ -123,7 +134,7 @@ export default function Select({
</Checkbox>
</Col>
<Col span={24} className={`${prefix}__menu`}>
<Checkbox.Group value={value}>
<Checkbox.Group value={selected}>
<List
data={options}
itemKey="value"
Expand Down

0 comments on commit 89426ce

Please sign in to comment.