Commit 9e09868a authored by Andy-bubu's avatar Andy-bubu

fix: stabilize controlled autocomplete selection

parent 5f6746ea
import { ChevronDown, Close, FIELD_SUFFIX_ICON_STYLE, Search } from '@/components/AppIcons';
import { Check, ChevronDown, Close, FIELD_SUFFIX_ICON_STYLE, Search } from '@/components/AppIcons';
import { Autocomplete, Label, ListBox, SearchField, Spinner } from '@heroui/react';
function defaultFilter(textValue, inputValue) {
......@@ -7,6 +7,7 @@ function defaultFilter(textValue, inputValue) {
}
const optionKey = (option) => option?.id ?? option?.value;
const toSelectionKey = (value) => String(value ?? '');
export default function XinyuanAutocomplete({
'aria-label': ariaLabelProp, ariaLabel, className, emptyText = '暂无可选项', errorText = '', filter,
......@@ -16,24 +17,49 @@ export default function XinyuanAutocomplete({
selectionMode = 'single', shouldFilter = true, valueText, variant = 'secondary',
}) {
const effectiveAriaLabel = ariaLabelProp || ariaLabel || placeholder;
const selectedSet = new Set(selectionMode === 'multiple' ? Array.from(selectedKeys || []).map(String) : selectedKey === null || selectedKey === undefined || selectedKey === '' ? [] : [String(selectedKey)]);
const selectedOptions = options.filter((option) => selectedSet.has(String(optionKey(option))));
const selectedKeySet = selectionMode === 'multiple'
? new Set(selectedKeys || [])
: new Set(selectedKey === null || selectedKey === undefined || selectedKey === '' ? [] : [selectedKey]);
const selectedKeyStringSet = new Set(Array.from(selectedKeySet).map(toSelectionKey));
const selectedOptions = options.filter((option) => selectedKeyStringSet.has(toSelectionKey(optionKey(option))));
const resolvedValueText = valueText ?? (selectedOptions.length ? selectedOptions.map((option) => option.label).join('、') : placeholder);
const selectionProps = selectionMode === 'multiple' ? { selectedKeys } : { selectedKey: selectedKey || null };
const rootSelectionProps = selectionMode === 'multiple' ? { selectedKeys } : { selectedKey: selectedKey || null };
const listBoxSelectionProps = selectionMode === 'multiple' ? { selectionMode, selectedKeys: selectedKeyStringSet } : {};
const visibleOptions = isLoading || errorText ? [] : options;
const handleMultipleOptionPress = (option) => {
if (selectionMode !== 'multiple' || !onSelectionChange || (option.isDisabled ?? option.disabled)) return;
const key = toSelectionKey(optionKey(option));
if (!key) return;
const nextKeys = new Set(Array.from(selectedKeyStringSet).filter(Boolean));
if (nextKeys.has(key)) nextKeys.delete(key);
else nextKeys.add(key);
onSelectionChange(nextKeys);
};
return (
<Autocomplete {...selectionProps} aria-label={effectiveAriaLabel} className={className} fullWidth={fullWidth} isDisabled={isDisabled} isOpen={isOpen} isRequired={isRequired} name={name} selectionMode={selectionMode} variant={variant} onOpenChange={onOpenChange} onSelectionChange={(key) => onSelectionChange?.(key ? String(key) : '')}>
<Autocomplete {...rootSelectionProps} aria-label={effectiveAriaLabel} className={className} fullWidth={fullWidth} isDisabled={isDisabled} isOpen={isOpen} isRequired={isRequired} name={name} selectionMode={selectionMode} variant={variant} onOpenChange={onOpenChange} onSelectionChange={onSelectionChange}>
<Autocomplete.Trigger>
<Autocomplete.Value><span className="block min-w-0 truncate" title={resolvedValueText}>{resolvedValueText}</span></Autocomplete.Value>
{selectionMode === 'single' && selectedKey ? <Autocomplete.ClearButton aria-label="清空选择" onPress={() => onSelectionChange?.('')}><Close className="grid size-4 place-items-center leading-none [&>svg]:block [&>svg]:size-4" size={16} style={FIELD_SUFFIX_ICON_STYLE} /></Autocomplete.ClearButton> : null}
{selectionMode === 'single' && selectedKey ? <Autocomplete.ClearButton aria-label="清空选择" onClick={() => onSelectionChange?.('')}><Close className="grid size-4 place-items-center leading-none [&>svg]:block [&>svg]:size-4" size={16} style={FIELD_SUFFIX_ICON_STYLE} /></Autocomplete.ClearButton> : null}
<Autocomplete.Indicator><ChevronDown size={16} style={FIELD_SUFFIX_ICON_STYLE} /></Autocomplete.Indicator>
</Autocomplete.Trigger>
<Autocomplete.Popover placement={placement}>
<Autocomplete.Filter filter={shouldFilter ? filter || defaultFilter : () => true} inputValue={inputValue} onInputChange={onInputChange}>
{popoverHeader}
<SearchField aria-label={searchPlaceholder} fullWidth variant={variant}><SearchField.Group><SearchField.SearchIcon><Search size={16} /></SearchField.SearchIcon><SearchField.Input autoFocus placeholder={searchPlaceholder} /><SearchField.ClearButton aria-label="清空搜索"><Close size={14} /></SearchField.ClearButton></SearchField.Group></SearchField>
<ListBox aria-label={effectiveAriaLabel} items={visibleOptions} renderEmptyState={() => <div className="grid min-h-20 place-items-center px-3 py-4" data-slot="empty-state">{isLoading ? <Spinner size="sm" /> : errorText || emptyText}</div>}>
{(option) => <ListBox.Item id={String(optionKey(option))} isDisabled={option.isDisabled ?? option.disabled} textValue={option.textValue || option.label}>{renderOption ? renderOption(option) : <Label className="truncate" title={option.label}>{option.label}</Label>}<ListBox.ItemIndicator /></ListBox.Item>}
<ListBox {...listBoxSelectionProps} aria-label={effectiveAriaLabel} items={visibleOptions} renderEmptyState={() => <div className="grid min-h-20 place-items-center px-3 py-4" data-slot="empty-state">{isLoading ? <Spinner size="sm" /> : errorText || emptyText}</div>}>
{(option) => {
const isSelected = selectedKeyStringSet.has(toSelectionKey(optionKey(option)));
return (
<ListBox.Item id={toSelectionKey(optionKey(option))} isDisabled={option.isDisabled ?? option.disabled} textValue={option.textValue || option.label} onAction={selectionMode === 'multiple' ? () => handleMultipleOptionPress(option) : undefined}>
{renderOption ? renderOption(option) : <Label className="truncate" title={option.label}>{option.label}</Label>}
{selectionMode === 'multiple' ? <span aria-hidden="true" className="ml-auto inline-flex w-5 justify-center text-foreground">{isSelected ? <Check size={14} /> : null}</span> : <ListBox.ItemIndicator />}
</ListBox.Item>
);
}}
</ListBox>
</Autocomplete.Filter>
</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