Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/dropdown/__tests__/__snapshots__/dropdown.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,9 @@ exports[`Test Dropdown.Select Component Should match snapshot 1`] = `
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox ant-checkbox-indeterminate"
class="ant-checkbox"
>
<input
aria-checked="mixed"
class="ant-checkbox-input"
type="checkbox"
value=""
Expand Down
8 changes: 4 additions & 4 deletions src/dropdown/__tests__/dropdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,8 @@ describe('Test Dropdown.Select Component', () => {
act(() => {
jest.runAllTimers();
});
// Should be indeterminate
expect(getByText('全选').previousElementSibling?.className).toContain(
// Only disabled item selected → not indeterminate (no enabled items selected)
expect(getByText('全选').previousElementSibling?.className).not.toContain(
'ant-checkbox-indeterminate'
);

Expand Down Expand Up @@ -347,9 +347,9 @@ describe('Test Dropdown.Select Component', () => {
</Button>
</Dropdown.Select>
);
// Should be indeterminate
// All enabled items selected → checked
expect(getByText('全选').previousElementSibling?.className).toContain(
'ant-checkbox-indeterminate'
'ant-checkbox-checked'
);

rerender(
Expand Down
147 changes: 93 additions & 54 deletions src/dropdown/select.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ReactNode, useEffect, useMemo, useState } from 'react';
import React, { ReactNode, useCallback, 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 {
Expand All @@ -7,7 +7,6 @@ import type {
CheckboxValueType,
} from 'antd/lib/checkbox/Group';
import classNames from 'classnames';
import { isEqual } from 'lodash-es';
import List from 'rc-virtual-list';

import useLocale from '../locale/useLocale';
Expand Down Expand Up @@ -38,24 +37,102 @@ export default function Select({

const locale = useLocale('Dropdown');

const handleCheckedAll = (e: CheckboxChangeEvent) => {
if (e.target.checked) {
setSelected(options?.map((i) => i.value) || []);
} else {
handleReset();
useEffect(() => {
if (value !== undefined) {
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;
}) || []
);
}, [rawOptions]);

/**
* The "derived metadata" of the selected data
* It does not directly participate in rendering but is only used for logical judgment
*
* Purpose:
* - Clearly distinguish enabled / disabled
* - Prevent disabled items from being accidentally selected / reset
*/
const selectionMeta = useMemo(() => {
const enabled = new Set<CheckboxValueType>();
const disabled = new Set<CheckboxValueType>();

options.forEach((o) => {
if (o.disabled) {
disabled.add(o.value);
} else {
enabled.add(o.value);
}
});

const selectedEnabled: CheckboxValueType[] = [];
const selectedDisabled: CheckboxValueType[] = [];

selected.forEach((v) => {
if (enabled.has(v)) {
selectedEnabled.push(v);
}
if (disabled.has(v)) {
selectedDisabled.push(v);
}
});

return {
/** All selectable (non-disabled) values */
enabledValues: [...enabled],
/** All disabled values */
disabledValues: [...disabled],
/** Currently selected enabled items */
selectedEnabled,
/** Currently selected disabled items (for reset retention) */
selectedDisabled,
/** All enabled items are selected */
checkAll: enabled.size > 0 && selectedEnabled.length === enabled.size,
/** Partial enabled items are selected */
indeterminate: selectedEnabled.length > 0 && selectedEnabled.length < enabled.size,
/**
* Whether to disable the Reset button
* Only disabled when:
* - All currently selected items are disabled
*/
resetDisabled: selected.length > 0 && selected.every((v) => disabled.has(v)),
};
}, [options, selected]);

const handleReset = useCallback(() => {
setSelected(selectionMeta.selectedDisabled);
}, [selectionMeta.selectedDisabled]);

const handleCheckedAll = useCallback(
(e: CheckboxChangeEvent) => {
if (e.target.checked) {
setSelected([...selectionMeta.enabledValues, ...selectionMeta.selectedDisabled]);
} else {
handleReset();
}
},
[selectionMeta, handleReset]
);

const handleSubmit = () => {
onChange?.(selected);
setVisible(false);
};

const handleReset = () => {
// Clear checked but disabled item
setSelected(disabledValue);
};

const handleChange = (e: CheckboxChangeEvent) => {
const { checked, value } = e.target;
const next = checked ? [...selected, value] : selected?.filter((i) => i !== value);
Expand Down Expand Up @@ -83,55 +160,17 @@ export default function Select({
}
};

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;
}) || []
);
}, [rawOptions]);

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 =
!!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 =
!!selected?.length &&
!isEqual(options.map((i) => i.value).sort(), [...selected].sort()) &&
options.some((o) => selected.includes(o.value));

const overlay = (
<>
<Row>
<Col span={24} className={`${prefix}__col`}>
<Checkbox
onChange={handleCheckedAll}
checked={checkAll}
indeterminate={indeterminate}
checked={selectionMeta.checkAll}
indeterminate={selectionMeta.indeterminate}
>
{locale.selectAll}
</Checkbox>
Expand Down Expand Up @@ -171,7 +210,7 @@ export default function Select({
</Col>
</Row>
<Space size={8} className={`${prefix}__btns`}>
<Button size="small" disabled={resetDisabled} onClick={handleReset}>
<Button size="small" disabled={selectionMeta.resetDisabled} onClick={handleReset}>
{locale.resetText}
</Button>
<Button size="small" type="primary" onClick={handleSubmit}>
Expand Down
Loading