Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
P
project_manager_xy
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
project_manager_xy
Commits
677c3711
Commit
677c3711
authored
Aug 27, 2026
by
Andy-bubu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: stabilize controlled autocomplete selections
parent
963009de
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
92 additions
and
23 deletions
+92
-23
XinyuanAutocomplete.jsx
src/components/XinyuanAutocomplete.jsx
+86
-19
XinyuanUserAutocomplete.jsx
src/components/XinyuanUserAutocomplete.jsx
+4
-2
ProjectBookingWorkspace.jsx
src/pages/project_manager_xy/ProjectBookingWorkspace.jsx
+2
-2
No files found.
src/components/XinyuanAutocomplete.jsx
View file @
677c3711
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
(
select
ionMode
===
'multiple'
?
normalizedValue
:
normalizedValue
===
null
?
[]
:
[
normalizedValue
]
,
const
selectedKeySet
=
getSelectedKeySet
(
selectionMode
,
select
edKey
,
selectedKeys
,
);
const
selectedKeyStringSet
=
new
Set
(
Array
.
from
(
selectedKeySet
).
map
(
toSelectionKey
)
,
);
const
selectedOptions
=
options
.
filter
((
option
)
=>
selectedKeyS
et
.
has
(
String
(
option
.
id
)),
selectedKeyS
tringSet
.
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
>
...
...
src/components/XinyuanUserAutocomplete.jsx
View file @
677c3711
...
...
@@ -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
)
=>
{
on
Selection
Change=
{
(
keys
)
=>
{
const
nextIds
=
multiple
?
Array
.
from
(
keys
||
[]).
map
(
String
)
:
keys
?
[
String
(
keys
)]
:
[];
...
...
src/pages/project_manager_xy/ProjectBookingWorkspace.jsx
View file @
677c3711
...
...
@@ -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
)
:
''
)
}
on
Selection
Change=
{
(
key
)
=>
onChange
?.(
key
?
String
(
key
)
:
''
)
}
/>
);
}
...
...
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