@@ -45,6 +45,7 @@ interface Data {
45
45
value : string ;
46
46
disabled : boolean ;
47
47
textValue : string ;
48
+ isVisible ?: boolean ;
48
49
}
49
50
50
51
interface OptionData extends Data {
@@ -406,8 +407,12 @@ const ComboxboxTextInput = React.forwardRef<ComboboxInputElement, TextInputProps
406
407
ref = { composedRefs }
407
408
onKeyDown = { composeEventHandlers ( props . onKeyDown , ( event ) => {
408
409
if ( [ 'ArrowUp' , 'ArrowDown' , 'Home' , 'End' ] . includes ( event . key ) ) {
410
+ if ( ! context . open ) {
411
+ handleOpen ( ) ;
412
+ }
413
+
409
414
setTimeout ( ( ) => {
410
- const items = getItems ( ) . filter ( ( item ) => ! item . disabled ) ;
415
+ const items = getItems ( ) . filter ( ( item ) => ! item . disabled && item . isVisible ) ;
411
416
let candidateNodes = items . map ( ( item ) => item . ref . current ! ) ;
412
417
413
418
if ( [ 'ArrowUp' , 'End' ] . includes ( event . key ) ) {
@@ -491,10 +496,7 @@ const ComboxboxTextInput = React.forwardRef<ComboboxInputElement, TextInputProps
491
496
}
492
497
} ) }
493
498
onKeyUp = { composeEventHandlers ( props . onKeyUp , ( event ) => {
494
- if (
495
- ! context . open &&
496
- ( isPrintableCharacter ( event . key ) || [ 'ArrowUp' , 'ArrowDown' , 'Home' , 'End' , 'Backspace' ] . includes ( event . key ) )
497
- ) {
499
+ if ( ! context . open && ( isPrintableCharacter ( event . key ) || [ 'Backspace' ] . includes ( event . key ) ) ) {
498
500
handleOpen ( ) ;
499
501
}
500
502
@@ -859,6 +861,7 @@ interface ComboboxItemContextValue {
859
861
onTextValueChange ( node : HTMLSpanElement | null ) : void ;
860
862
textId : string ;
861
863
isSelected : boolean ;
864
+ textValue : string ;
862
865
}
863
866
864
867
const [ ComboboxItemProvider , useComboboxItemContext ] = createContext < ComboboxItemContextValue > ( ITEM_NAME ) ;
@@ -867,52 +870,29 @@ const [ComboboxItemProvider, useComboboxItemContext] = createContext<ComboboxIte
867
870
* ComboboxItem
868
871
* -----------------------------------------------------------------------------------------------*/
869
872
870
- type ComboboxItemElement = React . ElementRef < typeof Primitive . div > ;
873
+ type ComboboxItemElement = ComboboxItemImplElement ;
871
874
872
- interface ItemProps extends PrimitiveDivProps {
873
- value : string ;
874
- disabled ?: boolean ;
875
+ interface ItemProps extends ItemImplProps {
875
876
textValue ?: string ;
876
877
}
877
878
878
879
export const ComboboxItem = React . forwardRef < ComboboxItemElement , ItemProps > ( ( props , forwardedRef ) => {
879
- const { value, disabled = false , textValue : textValueProp , ... restProps } = props ;
880
- const itemRef = React . useRef < HTMLDivElement > ( null ) ;
880
+ const { value, disabled = false , textValue : textValueProp } = props ;
881
+ const [ fragment , setFragment ] = React . useState < DocumentFragment > ( ) ;
881
882
882
- const composedRefs = useComposedRefs ( forwardedRef , itemRef ) ;
883
+ // setting the fragment in `useLayoutEffect` as `DocumentFragment` doesn't exist on the server
884
+ useLayoutEffect ( ( ) => {
885
+ setFragment ( new DocumentFragment ( ) ) ;
886
+ } , [ ] ) ;
883
887
884
- const { getItems } = useCollection ( undefined ) ;
885
- const {
886
- onTextValueChange,
887
- textValue : contextTextValue ,
888
- visuallyFocussedItem,
889
- ...context
890
- } = useComboboxContext ( ITEM_NAME ) ;
888
+ const { onTextValueChange, textValue : contextTextValue , ...context } = useComboboxContext ( ITEM_NAME ) ;
891
889
892
890
const textId = useId ( ) ;
893
891
894
892
const [ textValue , setTextValue ] = React . useState ( textValueProp ?? '' ) ;
895
893
896
- const isFocused = React . useMemo ( ( ) => {
897
- return visuallyFocussedItem === getItems ( ) . find ( ( item ) => item . ref . current === itemRef . current ) ?. ref . current ;
898
- } , [ getItems , visuallyFocussedItem ] ) ;
899
-
900
894
const isSelected = context . value === value ;
901
895
902
- const handleSelect = ( ) => {
903
- if ( ! disabled ) {
904
- context . onValueChange ( value ) ;
905
- onTextValueChange ( textValue ) ;
906
- context . onOpenChange ( false ) ;
907
-
908
- if ( context . autocomplete === 'both' ) {
909
- context . onFilterValueChange ( textValue ) ;
910
- }
911
-
912
- context . trigger ?. focus ( { preventScroll : true } ) ;
913
- }
914
- } ;
915
-
916
896
const { startsWith } = useFilter ( context . locale , { sensitivity : 'base' } ) ;
917
897
918
898
const handleTextValueChange = React . useCallback ( ( node : HTMLSpanElement | null ) => {
@@ -931,44 +911,119 @@ export const ComboboxItem = React.forwardRef<ComboboxItemElement, ItemProps>((pr
931
911
}
932
912
} , [ textValue , isSelected , contextTextValue , onTextValueChange ] ) ;
933
913
934
- const id = useId ( ) ;
935
-
936
- if ( context . autocomplete === 'list' && textValue && contextTextValue && ! startsWith ( textValue , contextTextValue ) ) {
937
- return null ;
938
- }
939
-
940
914
if (
941
- context . autocomplete === 'both' &&
942
- textValue &&
943
- context . filterValue &&
944
- ! startsWith ( textValue , context . filterValue )
915
+ ( context . autocomplete === 'both' &&
916
+ textValue &&
917
+ context . filterValue &&
918
+ ! startsWith ( textValue , context . filterValue ) ) ||
919
+ ( context . autocomplete === 'list' && textValue && contextTextValue && ! startsWith ( textValue , contextTextValue ) )
945
920
) {
946
- return null ;
921
+ return fragment
922
+ ? ReactDOM . createPortal (
923
+ < ComboboxItemProvider
924
+ textId = { textId }
925
+ onTextValueChange = { handleTextValueChange }
926
+ isSelected = { isSelected }
927
+ textValue = { textValue }
928
+ >
929
+ < Collection . ItemSlot
930
+ scope = { undefined }
931
+ value = { value }
932
+ textValue = { textValue }
933
+ disabled = { disabled }
934
+ type = "option"
935
+ isVisible = { false }
936
+ >
937
+ < ComboboxItemImpl ref = { forwardedRef } { ...props } />
938
+ </ Collection . ItemSlot >
939
+ </ ComboboxItemProvider > ,
940
+ fragment ,
941
+ )
942
+ : null ;
947
943
}
948
944
949
945
return (
950
- < ComboboxItemProvider textId = { textId } onTextValueChange = { handleTextValueChange } isSelected = { isSelected } >
951
- < Collection . ItemSlot scope = { undefined } value = { value } textValue = { textValue } disabled = { disabled } type = "option" >
952
- < Primitive . div
953
- role = "option"
954
- aria-labelledby = { textId }
955
- data-highlighted = { isFocused ? '' : undefined }
956
- // `isFocused` caveat fixes stuttering in VoiceOver
957
- aria-selected = { isSelected && isFocused }
958
- data-state = { isSelected ? 'checked' : 'unchecked' }
959
- aria-disabled = { disabled || undefined }
960
- data-disabled = { disabled ? '' : undefined }
961
- tabIndex = { disabled ? undefined : - 1 }
962
- { ...restProps }
963
- id = { id }
964
- ref = { composedRefs }
965
- onPointerUp = { composeEventHandlers ( restProps . onPointerUp , handleSelect ) }
966
- />
946
+ < ComboboxItemProvider
947
+ textId = { textId }
948
+ onTextValueChange = { handleTextValueChange }
949
+ isSelected = { isSelected }
950
+ textValue = { textValue }
951
+ >
952
+ < Collection . ItemSlot
953
+ scope = { undefined }
954
+ value = { value }
955
+ textValue = { textValue }
956
+ disabled = { disabled }
957
+ type = "option"
958
+ isVisible
959
+ >
960
+ < ComboboxItemImpl ref = { forwardedRef } { ...props } />
967
961
</ Collection . ItemSlot >
968
962
</ ComboboxItemProvider >
969
963
) ;
970
964
} ) ;
971
965
966
+ /* -------------------------------------------------------------------------------------------------
967
+ * ComboboxItemImpl
968
+ * -----------------------------------------------------------------------------------------------*/
969
+
970
+ const ITEM_IMPL_NAME = 'ComboboxItemImpl' ;
971
+
972
+ type ComboboxItemImplElement = React . ElementRef < typeof Primitive . div > ;
973
+
974
+ interface ItemImplProps extends PrimitiveDivProps {
975
+ value : string ;
976
+ disabled ?: boolean ;
977
+ }
978
+
979
+ const ComboboxItemImpl = React . forwardRef < ComboboxItemImplElement , ItemImplProps > ( ( props , forwardedRef ) => {
980
+ const { value, disabled = false , ...restProps } = props ;
981
+ const itemRef = React . useRef < HTMLDivElement > ( null ) ;
982
+ const composedRefs = useComposedRefs ( forwardedRef , itemRef ) ;
983
+
984
+ const { getItems } = useCollection ( undefined ) ;
985
+ const { onTextValueChange, visuallyFocussedItem, ...context } = useComboboxContext ( ITEM_NAME ) ;
986
+ const { isSelected, textValue, textId } = useComboboxItemContext ( ITEM_IMPL_NAME ) ;
987
+
988
+ const handleSelect = ( ) => {
989
+ if ( ! disabled ) {
990
+ context . onValueChange ( value ) ;
991
+ onTextValueChange ( textValue ) ;
992
+ context . onOpenChange ( false ) ;
993
+
994
+ if ( context . autocomplete === 'both' ) {
995
+ context . onFilterValueChange ( textValue ) ;
996
+ }
997
+
998
+ context . trigger ?. focus ( { preventScroll : true } ) ;
999
+ }
1000
+ } ;
1001
+
1002
+ const isFocused = React . useMemo ( ( ) => {
1003
+ return visuallyFocussedItem === getItems ( ) . find ( ( item ) => item . ref . current === itemRef . current ) ?. ref . current ;
1004
+ } , [ getItems , visuallyFocussedItem ] ) ;
1005
+
1006
+ const id = useId ( ) ;
1007
+
1008
+ return (
1009
+ < Primitive . div
1010
+ role = "option"
1011
+ aria-labelledby = { textId }
1012
+ data-highlighted = { isFocused ? '' : undefined }
1013
+ // `isFocused` caveat fixes stuttering in VoiceOver
1014
+ aria-selected = { isSelected && isFocused }
1015
+ data-state = { isSelected ? 'checked' : 'unchecked' }
1016
+ aria-disabled = { disabled || undefined }
1017
+ data-disabled = { disabled ? '' : undefined }
1018
+ tabIndex = { disabled ? undefined : - 1 }
1019
+ { ...restProps }
1020
+ id = { id }
1021
+ ref = { composedRefs }
1022
+ onPointerUp = { composeEventHandlers ( restProps . onPointerUp , handleSelect ) }
1023
+ />
1024
+ ) ;
1025
+ } ) ;
1026
+
972
1027
/* -------------------------------------------------------------------------------------------------
973
1028
* ComboboxItemText
974
1029
* -----------------------------------------------------------------------------------------------*/
@@ -1009,7 +1064,7 @@ const NO_VALUE_FOUND_NAME = 'ComboboxNoValueFound';
1009
1064
interface NoValueFoundProps extends PrimitiveDivProps { }
1010
1065
1011
1066
const ComboboxNoValueFound = React . forwardRef < HTMLDivElement , NoValueFoundProps > ( ( props , ref ) => {
1012
- const { textValue = '' , locale } = useComboboxContext ( NO_VALUE_FOUND_NAME ) ;
1067
+ const { textValue = '' , filterValue = '' , locale, autocomplete } = useComboboxContext ( NO_VALUE_FOUND_NAME ) ;
1013
1068
const [ items , setItems ] = React . useState < CollectionData [ ] > ( [ ] ) ;
1014
1069
const { subscribe } = useCollection ( undefined ) ;
1015
1070
@@ -1029,7 +1084,13 @@ const ComboboxNoValueFound = React.forwardRef<HTMLDivElement, NoValueFoundProps>
1029
1084
} ;
1030
1085
} , [ subscribe ] ) ;
1031
1086
1032
- if ( items . some ( ( item ) => startsWith ( item . textValue , textValue ) ) ) {
1087
+ if ( items . length === 0 ) return null ;
1088
+
1089
+ if ( autocomplete === 'list' && items . some ( ( item ) => startsWith ( item . textValue , textValue ) ) ) {
1090
+ return null ;
1091
+ }
1092
+
1093
+ if ( autocomplete === 'both' && items . some ( ( item ) => startsWith ( item . textValue , filterValue ) ) ) {
1033
1094
return null ;
1034
1095
}
1035
1096
@@ -1098,6 +1159,7 @@ const ComboboxCreateItem = React.forwardRef<ComboboxItemElement, CreateItemProps
1098
1159
value = { textValue ?? '' }
1099
1160
textValue = { textValue ?? '' }
1100
1161
disabled = { disabled }
1162
+ isVisible
1101
1163
type = "create"
1102
1164
>
1103
1165
< Primitive . div
0 commit comments