Commit 2a9f1577 authored by Andy-bubu's avatar Andy-bubu

fix: stabilize controlled autocomplete selection

parent 082b0507
import { DownOutlined, SearchOutlined } from '@ant-design/icons'; import { CheckOutlined, DownOutlined, SearchOutlined } from '@ant-design/icons';
import { import {
Autocomplete, Autocomplete,
Label, Label,
...@@ -17,6 +17,10 @@ function defaultFilter(textValue, inputValue) { ...@@ -17,6 +17,10 @@ function defaultFilter(textValue, inputValue) {
return !query || String(textValue || '').toLocaleLowerCase().includes(query); return !query || String(textValue || '').toLocaleLowerCase().includes(query);
} }
function toSelectionKey(value) {
return String(value ?? '');
}
export default function XinyuanAutocomplete({ export default function XinyuanAutocomplete({
'aria-label': ariaLabel, 'aria-label': ariaLabel,
className, className,
...@@ -32,36 +36,95 @@ export default function XinyuanAutocomplete({ ...@@ -32,36 +36,95 @@ export default function XinyuanAutocomplete({
placeholder = '请选择', placeholder = '请选择',
searchPlaceholder = '输入关键词搜索', searchPlaceholder = '输入关键词搜索',
selectedKey = '', selectedKey = '',
selectedKeys,
selectionMode = 'single',
valueText,
variant = 'secondary', variant = 'secondary',
}) { }) {
const normalizedOptions = options.map((option) => ({ const normalizedOptions = options.map((option) => {
id: option.value === '' ? EMPTY_KEY : String(option.value), const optionValue = Object.prototype.hasOwnProperty.call(option, 'id')
isDisabled: Boolean(option.disabled), ? option.id
label: String(option.label), : option.value;
}));
return {
...option,
id: optionValue === '' ? EMPTY_KEY : toSelectionKey(optionValue),
isDisabled: Boolean(option.isDisabled ?? option.disabled),
label: String(option.label),
};
});
const normalizedKey = selectedKey === '' || selectedKey == null const normalizedKey = selectedKey === '' || selectedKey == null
? EMPTY_KEY ? EMPTY_KEY
: String(selectedKey); : toSelectionKey(selectedKey);
const selectedOption = normalizedOptions.find((option) => option.id === normalizedKey); const selectedKeySet = selectionMode === 'multiple'
? new Set(selectedKeys || [])
: new Set([normalizedKey]);
const selectedKeyStringSet = new Set(
Array.from(selectedKeySet).map(toSelectionKey),
);
const selectedOptions = normalizedOptions.filter((option) =>
selectedKeyStringSet.has(toSelectionKey(option.id)),
);
const resolvedValueText = valueText ?? (
selectedOptions.length > 0
? selectedOptions.map((option) => option.label).join('、')
: placeholder
);
const rootSelectionProps = selectionMode === 'multiple'
? { selectedKeys: selectedKeyStringSet }
: { selectedKey: normalizedKey };
const listBoxSelectionProps = selectionMode === 'multiple'
? {
selectedKeys: selectedKeyStringSet,
selectionMode,
}
: {};
const handleMultipleOptionPress = (option) => {
if (
selectionMode !== 'multiple'
|| !onSelectionChange
|| option?.isDisabled
|| option?.id === EMPTY_KEY
) {
return;
}
const optionKey = toSelectionKey(option.id);
if (!optionKey) return;
const nextKeys = new Set(
Array.from(selectedKeyStringSet).filter(Boolean),
);
if (nextKeys.has(optionKey)) {
nextKeys.delete(optionKey);
} else {
nextKeys.add(optionKey);
}
onSelectionChange(nextKeys);
};
return ( return (
<Autocomplete <Autocomplete
{...rootSelectionProps}
aria-label={ariaLabel || label || placeholder} aria-label={ariaLabel || label || placeholder}
className={className} className={className}
fullWidth fullWidth
isDisabled={isDisabled} isDisabled={isDisabled}
isRequired={isRequired} isRequired={isRequired}
selectedKey={normalizedKey} selectionMode={selectionMode}
variant={variant} variant={variant}
onSelectionChange={(key) => { onSelectionChange={selectionMode === 'multiple'
onSelectionChange?.(key === EMPTY_KEY ? '' : String(key ?? '')); ? onSelectionChange
}} : (key) => {
onSelectionChange?.(key === EMPTY_KEY ? '' : toSelectionKey(key));
}}
> >
{label ? <Label>{label}</Label> : null} {label ? <Label>{label}</Label> : null}
<Autocomplete.Trigger> <Autocomplete.Trigger>
<Autocomplete.Value> <Autocomplete.Value>
<span className="block min-w-0 truncate"> <span className="block min-w-0 truncate">
{selectedOption?.label || placeholder} {resolvedValueText}
</span> </span>
</Autocomplete.Value> </Autocomplete.Value>
<Autocomplete.Indicator> <Autocomplete.Indicator>
...@@ -84,6 +147,7 @@ export default function XinyuanAutocomplete({ ...@@ -84,6 +147,7 @@ export default function XinyuanAutocomplete({
</SearchField.Group> </SearchField.Group>
</SearchField> </SearchField>
<ListBox <ListBox
{...listBoxSelectionProps}
aria-label={ariaLabel || label || placeholder} aria-label={ariaLabel || label || placeholder}
items={isLoading ? [] : normalizedOptions} items={isLoading ? [] : normalizedOptions}
renderEmptyState={() => ( renderEmptyState={() => (
...@@ -92,16 +156,34 @@ export default function XinyuanAutocomplete({ ...@@ -92,16 +156,34 @@ export default function XinyuanAutocomplete({
</div> </div>
)} )}
> >
{(option) => ( {(option) => {
<ListBox.Item const isSelected = selectedKeyStringSet.has(
id={option.id} toSelectionKey(option.id),
isDisabled={option.isDisabled} );
textValue={option.label}
> return (
<Label className="truncate">{option.label}</Label> <ListBox.Item
<ListBox.ItemIndicator /> id={option.id}
</ListBox.Item> isDisabled={option.isDisabled}
)} textValue={option.label}
{...(selectionMode === 'multiple'
? { onPress: () => handleMultipleOptionPress(option) }
: {})}
>
<Label className="truncate">{option.label}</Label>
{selectionMode === 'multiple' ? (
<span
aria-hidden="true"
className="ml-auto inline-flex w-5 justify-center text-foreground"
>
{isSelected ? <CheckOutlined className="size-3.5" /> : null}
</span>
) : (
<ListBox.ItemIndicator />
)}
</ListBox.Item>
);
}}
</ListBox> </ListBox>
</Autocomplete.Filter> </Autocomplete.Filter>
</Autocomplete.Popover> </Autocomplete.Popover>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment