Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
I
incorrupt_party_build
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
David Yang
incorrupt_party_build
Commits
9e09868a
Commit
9e09868a
authored
Aug 27, 2026
by
Andy-bubu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: stabilize controlled autocomplete selection
parent
5f6746ea
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
34 additions
and
8 deletions
+34
-8
XinyuanAutocomplete.jsx
src/components/XinyuanAutocomplete.jsx
+34
-8
No files found.
src/components/XinyuanAutocomplete.jsx
View file @
9e09868a
import
{
ChevronDown
,
Close
,
FIELD_SUFFIX_ICON_STYLE
,
Search
}
from
'@/components/AppIcons'
;
import
{
Che
ck
,
Che
vronDown
,
Close
,
FIELD_SUFFIX_ICON_STYLE
,
Search
}
from
'@/components/AppIcons'
;
import
{
Autocomplete
,
Label
,
ListBox
,
SearchField
,
Spinner
}
from
'@heroui/react'
;
import
{
Autocomplete
,
Label
,
ListBox
,
SearchField
,
Spinner
}
from
'@heroui/react'
;
function
defaultFilter
(
textValue
,
inputValue
)
{
function
defaultFilter
(
textValue
,
inputValue
)
{
...
@@ -7,6 +7,7 @@ function defaultFilter(textValue, inputValue) {
...
@@ -7,6 +7,7 @@ function defaultFilter(textValue, inputValue) {
}
}
const
optionKey
=
(
option
)
=>
option
?.
id
??
option
?.
value
;
const
optionKey
=
(
option
)
=>
option
?.
id
??
option
?.
value
;
const
toSelectionKey
=
(
value
)
=>
String
(
value
??
''
);
export
default
function
XinyuanAutocomplete
({
export
default
function
XinyuanAutocomplete
({
'aria-label'
:
ariaLabelProp
,
ariaLabel
,
className
,
emptyText
=
'暂无可选项'
,
errorText
=
''
,
filter
,
'aria-label'
:
ariaLabelProp
,
ariaLabel
,
className
,
emptyText
=
'暂无可选项'
,
errorText
=
''
,
filter
,
...
@@ -16,24 +17,49 @@ export default function XinyuanAutocomplete({
...
@@ -16,24 +17,49 @@ export default function XinyuanAutocomplete({
selectionMode
=
'single'
,
shouldFilter
=
true
,
valueText
,
variant
=
'secondary'
,
selectionMode
=
'single'
,
shouldFilter
=
true
,
valueText
,
variant
=
'secondary'
,
})
{
})
{
const
effectiveAriaLabel
=
ariaLabelProp
||
ariaLabel
||
placeholder
;
const
effectiveAriaLabel
=
ariaLabelProp
||
ariaLabel
||
placeholder
;
const
selectedSet
=
new
Set
(
selectionMode
===
'multiple'
?
Array
.
from
(
selectedKeys
||
[]).
map
(
String
)
:
selectedKey
===
null
||
selectedKey
===
undefined
||
selectedKey
===
''
?
[]
:
[
String
(
selectedKey
)]);
const
selectedKeySet
=
selectionMode
===
'multiple'
const
selectedOptions
=
options
.
filter
((
option
)
=>
selectedSet
.
has
(
String
(
optionKey
(
option
))));
?
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
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
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
(
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
.
Trigger
>
<
Autocomplete
.
Value
><
span
className=
"block min-w-0 truncate"
title=
{
resolvedValueText
}
>
{
resolvedValueText
}
</
span
></
Autocomplete
.
Value
>
<
Autocomplete
.
Value
><
span
className=
"block min-w-0 truncate"
title=
{
resolvedValueText
}
>
{
resolvedValueText
}
</
span
></
Autocomplete
.
Value
>
{
selectionMode
===
'single'
&&
selectedKey
?
<
Autocomplete
.
ClearButton
aria
-
label=
"清空选择"
on
Press
=
{
()
=>
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=
"清空选择"
on
Click
=
{
()
=>
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
.
Indicator
><
ChevronDown
size=
{
16
}
style=
{
FIELD_SUFFIX_ICON_STYLE
}
/></
Autocomplete
.
Indicator
>
</
Autocomplete
.
Trigger
>
</
Autocomplete
.
Trigger
>
<
Autocomplete
.
Popover
placement=
{
placement
}
>
<
Autocomplete
.
Popover
placement=
{
placement
}
>
<
Autocomplete
.
Filter
filter=
{
shouldFilter
?
filter
||
defaultFilter
:
()
=>
true
}
inputValue=
{
inputValue
}
onInputChange=
{
onInputChange
}
>
<
Autocomplete
.
Filter
filter=
{
shouldFilter
?
filter
||
defaultFilter
:
()
=>
true
}
inputValue=
{
inputValue
}
onInputChange=
{
onInputChange
}
>
{
popoverHeader
}
{
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
>
<
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
>
}
>
<
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
)
=>
<
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
>
}
{
(
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
>
</
ListBox
>
</
Autocomplete
.
Filter
>
</
Autocomplete
.
Filter
>
</
Autocomplete
.
Popover
>
</
Autocomplete
.
Popover
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment