Commit 677c3711 authored by Andy-bubu's avatar Andy-bubu

fix: stabilize controlled autocomplete selections

parent 963009de
import {
ChevronDown,
Check,
Close,
FIELD_SUFFIX_ICON_STYLE,
Search,
......@@ -17,12 +18,16 @@ function defaultFilter(textValue, inputValue) {
return !query || String(textValue || '').toLocaleLowerCase().includes(query);
}
function normalizeValue(selectionMode, value) {
function getSelectedKeySet(selectionMode, selectedKey, selectedKeys) {
if (selectionMode === 'multiple') {
return Array.from(value || []).map(String);
return new Set(selectedKeys || []);
}
return value === null || value === undefined ? null : String(value);
return new Set(selectedKey === null || selectedKey === undefined ? [] : [selectedKey]);
}
function toSelectionKey(value) {
return String(value ?? '');
}
const FEEDBACK_OPTION_ID = '__xinyuan-autocomplete-feedback__';
......@@ -41,31 +46,32 @@ export default function XinyuanAutocomplete({
isOpen,
isRequired = false,
name,
onChange,
onInputChange,
onOpenChange,
onSelectionChange,
options = [],
placeholder = '请选择',
placement = 'bottom start',
popoverHeader,
renderOption,
searchPlaceholder = '输入关键词搜索',
selectedKey,
selectedKeys,
selectionMode = 'single',
shouldFilter = true,
value,
valueText,
variant,
}) {
const normalizedValue = normalizeValue(selectionMode, value);
const selectedKeySet = new Set(
selectionMode === 'multiple'
? normalizedValue
: normalizedValue === null
? []
: [normalizedValue],
const selectedKeySet = getSelectedKeySet(
selectionMode,
selectedKey,
selectedKeys,
);
const selectedKeyStringSet = new Set(
Array.from(selectedKeySet).map(toSelectionKey),
);
const selectedOptions = options.filter((option) =>
selectedKeySet.has(String(option.id)),
selectedKeyStringSet.has(toSelectionKey(option.id)),
);
const resolvedValueText =
valueText ??
......@@ -74,13 +80,49 @@ export default function XinyuanAutocomplete({
: placeholder);
const showClearButton =
isClearable && selectedKeySet.size > 0 && !isDisabled && !isRequired;
const rootSelectionProps =
selectionMode === 'multiple'
? { selectedKeys }
: { selectedKey };
const listBoxSelectionProps =
selectionMode === 'multiple'
? {
selectionMode,
selectedKeys,
}
: {};
const feedbackText = isLoading ? '加载中...' : errorText || emptyText;
const visibleOptions = options.length > 0 && !isLoading && !errorText
? options
: [{ id: FEEDBACK_OPTION_ID, isDisabled: true, isFeedback: true, label: feedbackText }];
const handleMultipleOptionPress = (option) => {
if (
selectionMode !== 'multiple' ||
!onSelectionChange ||
option?.isDisabled
) {
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 (
<Autocomplete
{...rootSelectionProps}
aria-label={ariaLabel || placeholder}
className={className}
fullWidth={fullWidth}
......@@ -89,10 +131,9 @@ export default function XinyuanAutocomplete({
isRequired={isRequired}
name={name}
selectionMode={selectionMode}
value={normalizedValue}
variant={variant}
onChange={onChange}
onOpenChange={onOpenChange}
onSelectionChange={onSelectionChange}
>
<Autocomplete.Trigger>
<Autocomplete.Value>
......@@ -103,7 +144,13 @@ export default function XinyuanAutocomplete({
{resolvedValueText}
</span>
</Autocomplete.Value>
{showClearButton ? <Autocomplete.ClearButton aria-label="清除选择" /> : null}
{showClearButton ? (
<Autocomplete.ClearButton
aria-hidden={selectionMode === 'multiple' ? false : undefined}
aria-label="清除选择"
data-empty={selectionMode === 'multiple' ? false : undefined}
/>
) : null}
<Autocomplete.Indicator>
<ChevronDown size={16} style={FIELD_SUFFIX_ICON_STYLE} />
</Autocomplete.Indicator>
......@@ -130,12 +177,22 @@ export default function XinyuanAutocomplete({
</SearchField.ClearButton>
</SearchField.Group>
</SearchField>
<ListBox aria-label={ariaLabel || placeholder} items={visibleOptions}>
{(option) => (
<ListBox
aria-label={ariaLabel || placeholder}
items={visibleOptions}
{...listBoxSelectionProps}
>
{(option) => {
const isSelected = selectedKeyStringSet.has(toSelectionKey(option.id));
return (
<ListBox.Item
id={option.id}
isDisabled={option.isDisabled}
textValue={option.textValue || option.label}
{...(selectionMode === 'multiple'
? { onPress: () => handleMultipleOptionPress(option) }
: {})}
>
{option.isFeedback ? (
<span className="flex min-h-16 items-center justify-center gap-2 text-sm text-muted">
......@@ -149,9 +206,19 @@ export default function XinyuanAutocomplete({
{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.Item>
);
}}
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
......
......@@ -87,6 +87,7 @@ export default function XinyuanUserAutocomplete({
const valueText = selectedLabels.length <= 2
? selectedLabels.join('、') || placeholder
: `${selectedLabels.slice(0, 2).join('、')} 等 ${selectedLabels.length} 人`;
const selectedKeySet = useMemo(() => new Set(selectedIds), [selectedIds]);
return (
<XinyuanAutocomplete
......@@ -98,12 +99,13 @@ export default function XinyuanUserAutocomplete({
options={autocompleteOptions}
placeholder={placeholder}
searchPlaceholder="输入姓名或账号"
selectedKey={multiple ? undefined : selectedIds[0] || null}
selectedKeys={multiple ? selectedKeySet : undefined}
selectionMode={multiple ? 'multiple' : 'single'}
shouldFilter={false}
value={multiple ? selectedIds : selectedIds[0] || null}
valueText={valueText}
variant={variant}
onChange={(keys) => {
onSelectionChange={(keys) => {
const nextIds = multiple
? Array.from(keys || []).map(String)
: keys ? [String(keys)] : [];
......
......@@ -109,9 +109,9 @@ function OptionAutocomplete({
isRequired={isRequired}
options={normalizedOptions}
placeholder={placeholder}
value={value ? String(value) : null}
selectedKey={value ? String(value) : null}
variant="secondary"
onChange={(key) => onChange?.(key ? String(key) : '')}
onSelectionChange={(key) => onChange?.(key ? String(key) : '')}
/>
);
}
......
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