Commit cb8971aa authored by Andy-bubu's avatar Andy-bubu

fix: stabilize multi-select autocomplete values

parent 9d6dea24
...@@ -112,7 +112,7 @@ export default function FormSelect({ ...@@ -112,7 +112,7 @@ export default function FormSelect({
selectionProps.selectedKeys = new Set(selectedValues.map(toOptionKey)); selectionProps.selectedKeys = new Set(selectedValues.map(toOptionKey));
selectionProps.onSelectionChange = (keys) => { selectionProps.onSelectionChange = (keys) => {
const nextKeys = const nextKeys =
keys === 'all' ? options.map((option) => option.value) : [...keys]; keys === 'all' ? options.map((option) => option.id) : [...keys];
emitChange(nextKeys); emitChange(nextKeys);
}; };
} else { } else {
......
import { import {
ChevronDown, ChevronDown,
Check,
Close, Close,
FIELD_SUFFIX_ICON_STYLE, FIELD_SUFFIX_ICON_STYLE,
Search, Search,
...@@ -31,6 +32,10 @@ function getSelectedKeySet(selectionMode, selectedKey, selectedKeys) { ...@@ -31,6 +32,10 @@ function getSelectedKeySet(selectionMode, selectedKey, selectedKeys) {
); );
} }
function toSelectionKey(value) {
return String(value ?? '');
}
export default function XinyuanAutocomplete({ export default function XinyuanAutocomplete({
'aria-label': ariaLabel, 'aria-label': ariaLabel,
className, className,
...@@ -72,15 +77,18 @@ export default function XinyuanAutocomplete({ ...@@ -72,15 +77,18 @@ export default function XinyuanAutocomplete({
selectedKey, selectedKey,
selectedKeys, selectedKeys,
); );
const selectedKeyStringSet = new Set(
Array.from(selectedKeySet).map(toSelectionKey),
);
const optionMap = new Map( const optionMap = new Map(
options.map((option) => [String(option.id), option]), options.map((option) => [toSelectionKey(option.id), option]),
); );
const selectedOptions = selectionMode === 'multiple' && selectedKeys === 'all' const selectedOptions = selectionMode === 'multiple' && selectedKeys === 'all'
? options ? options
: [...selectedKeySet].map((key) => : [...selectedKeyStringSet].map((key) =>
optionMap.get(String(key)) || { optionMap.get(key) || {
id: String(key), id: key,
label: String(key), label: key,
}, },
); );
const resolvedValueText = const resolvedValueText =
...@@ -90,8 +98,15 @@ export default function XinyuanAutocomplete({ ...@@ -90,8 +98,15 @@ export default function XinyuanAutocomplete({
: placeholder); : placeholder);
const rootSelectionProps = const rootSelectionProps =
selectionMode === 'multiple' selectionMode === 'multiple'
? { selectedKeys } ? { selectedKeys: selectedKeyStringSet }
: { selectedKey }; : { selectedKey };
const listBoxSelectionProps =
selectionMode === 'multiple'
? {
selectionMode,
selectedKeys: selectedKeyStringSet,
}
: {};
const rootOpenProps = typeof isOpen === 'boolean' ? { isOpen } : {}; const rootOpenProps = typeof isOpen === 'boolean' ? { isOpen } : {};
const activeFilter = shouldFilter ? filter || defaultFilter : null; const activeFilter = shouldFilter ? filter || defaultFilter : null;
const visibleOptions = isLoading || errorText const visibleOptions = isLoading || errorText
...@@ -106,34 +121,34 @@ export default function XinyuanAutocomplete({ ...@@ -106,34 +121,34 @@ export default function XinyuanAutocomplete({
const handleClear = () => { const handleClear = () => {
onSelectionChange?.(selectionMode === 'multiple' ? new Set() : null); onSelectionChange?.(selectionMode === 'multiple' ? new Set() : null);
}; };
const handleListSelectionChange = (keys) => { const handleMultipleOptionPress = (option) => {
if (selectionMode === 'multiple') { if (
onSelectionChange?.(keys); selectionMode !== 'multiple' ||
!onSelectionChange ||
option?.isDisabled
) {
return; return;
} }
const nextKey = keys === 'all' ? null : [...keys][0] || null; const optionKey = toSelectionKey(option.id);
onSelectionChange?.(nextKey); if (!optionKey) {
};
const handleOptionAction = (optionId) => {
const normalizedOptionId = String(optionId);
if (selectionMode === 'multiple') {
const nextKeys = new Set(selectedKeySet);
if (nextKeys.has(normalizedOptionId)) {
nextKeys.delete(normalizedOptionId);
} else {
nextKeys.add(normalizedOptionId);
}
onSelectionChange?.(nextKeys);
return; return;
} }
onSelectionChange?.(normalizedOptionId); const nextKeys = new Set(
Array.from(selectedKeyStringSet).filter(Boolean),
);
if (nextKeys.has(optionKey)) {
nextKeys.delete(optionKey);
} else {
nextKeys.add(optionKey);
}
onSelectionChange(nextKeys);
}; };
const handleTagRemove = (keys) => { const handleTagRemove = (keys) => {
const removedKeys = new Set([...keys].map((key) => String(key))); const removedKeys = new Set([...keys].map(toSelectionKey));
const nextKeys = new Set( const nextKeys = new Set(
[...selectedKeySet].filter((key) => !removedKeys.has(String(key))), [...selectedKeyStringSet].filter((key) => !removedKeys.has(key)),
); );
onSelectionChange?.(nextKeys); onSelectionChange?.(nextKeys);
}; };
...@@ -225,11 +240,12 @@ export default function XinyuanAutocomplete({ ...@@ -225,11 +240,12 @@ export default function XinyuanAutocomplete({
</SearchField> </SearchField>
<ListBox <ListBox
aria-label={ariaLabel || placeholder} aria-label={ariaLabel || placeholder}
dependencies={[
Array.from(selectedKeyStringSet).sort().join('\u0000'),
]}
items={visibleOptions} items={visibleOptions}
slot={null} slot={null}
selectedKeys={selectedKeySet} {...listBoxSelectionProps}
selectionMode={selectionMode}
onSelectionChange={handleListSelectionChange}
renderEmptyState={() => ( renderEmptyState={() => (
<div <div
className="grid min-h-20 place-items-center px-3 py-4" className="grid min-h-20 place-items-center px-3 py-4"
...@@ -239,21 +255,38 @@ export default function XinyuanAutocomplete({ ...@@ -239,21 +255,38 @@ 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.textValue || option.label}
onAction={() => handleOptionAction(option.id)} return (
> <ListBox.Item
{renderOption ? ( id={option.id}
renderOption(option) isDisabled={option.isDisabled}
) : ( textValue={option.textValue || option.label}
<Label className="block min-w-0 truncate">{option.label}</Label> {...(selectionMode === 'multiple'
)} ? { onPress: () => handleMultipleOptionPress(option) }
<ListBox.ItemIndicator /> : {})}
</ListBox.Item> >
)} {renderOption ? (
renderOption(option)
) : (
<Label className="block min-w-0 truncate">{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> </ListBox>
</Autocomplete.Filter> </Autocomplete.Filter>
</Autocomplete.Popover> </Autocomplete.Popover>
......
...@@ -259,6 +259,9 @@ function UserSelector({ value, onChange, multiple, variant }) { ...@@ -259,6 +259,9 @@ function UserSelector({ value, onChange, multiple, variant }) {
function FieldOptionSelect({ multiple, onChange, options, value, variant }) { function FieldOptionSelect({ multiple, onChange, options, value, variant }) {
const values = multiple ? toArray(value).map(String) : [String(value || '')]; const values = multiple ? toArray(value).map(String) : [String(value || '')];
const optionValueMap = new Map(
options.map((option) => [String(option.value), option.value]),
);
const selectedOptions = options.filter((option) => values.includes(String(option.value))); const selectedOptions = options.filter((option) => values.includes(String(option.value)));
const displayValue = selectedOptions.length const displayValue = selectedOptions.length
? selectedOptions.map((option) => option.label).join('、') ? selectedOptions.map((option) => option.label).join('、')
...@@ -266,8 +269,13 @@ function FieldOptionSelect({ multiple, onChange, options, value, variant }) { ...@@ -266,8 +269,13 @@ function FieldOptionSelect({ multiple, onChange, options, value, variant }) {
const selectProps = multiple const selectProps = multiple
? { ? {
selectedKeys: new Set(values), selectedKeys: new Set(values),
onSelectionChange: (keys) => onSelectionChange: (keys) => onChange(
onChange(keys === 'all' ? options.map((option) => option.value) : [...keys]), keys === 'all'
? options.map((option) => option.value)
: [...keys]
.map((key) => optionValueMap.get(String(key)))
.filter((item) => item !== undefined),
),
} }
: { : {
selectedKey: values[0] || null, selectedKey: values[0] || null,
......
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