1
- import type { ScrollIntoViewOptions } from '@primer/behaviors'
2
- import { scrollIntoView , FocusKeys } from '@primer/behaviors'
1
+ import { FocusKeys } from '@primer/behaviors'
3
2
import type { KeyboardEventHandler } from 'react'
4
3
import type React from 'react'
5
4
import { useCallback , useEffect , useRef , useState } from 'react'
@@ -10,7 +9,6 @@ import TextInput from '../TextInput'
10
9
import { get } from '../constants'
11
10
import { ActionList } from '../ActionList'
12
11
import type { GroupedListProps , ListPropsBase , ItemInput } from '../SelectPanel/types'
13
- import { useFocusZone } from '../hooks/useFocusZone'
14
12
import { useId } from '../hooks/useId'
15
13
import { useProvidedRefOrCreate } from '../hooks/useProvidedRefOrCreate'
16
14
import { useProvidedStateOrCreate } from '../hooks/useProvidedStateOrCreate'
@@ -20,14 +18,12 @@ import type {SxProp} from '../sx'
20
18
import type { FilteredActionListLoadingType } from './FilteredActionListLoaders'
21
19
import { FilteredActionListLoadingTypes , FilteredActionListBodyLoader } from './FilteredActionListLoaders'
22
20
import classes from './FilteredActionList.module.css'
23
-
21
+ import { ActionListContainerContext } from '../ActionList/ActionListContainerContext'
24
22
import { isValidElementType } from 'react-is'
25
23
import type { RenderItemFn } from '../deprecated/ActionList/List'
26
24
import { useAnnouncements } from './useAnnouncements'
27
25
import { clsx } from 'clsx'
28
26
29
- const menuScrollMargins : ScrollIntoViewOptions = { startMargin : 0 , endMargin : 8 }
30
-
31
27
export interface FilteredActionListProps
32
28
extends Partial < Omit < GroupedListProps , keyof ListPropsBase > > ,
33
29
ListPropsBase ,
@@ -37,7 +33,6 @@ export interface FilteredActionListProps
37
33
placeholderText ?: string
38
34
filterValue ?: string
39
35
onFilterChange : ( value : string , e : React . ChangeEvent < HTMLInputElement > ) => void
40
- onListContainerRefChanged ?: ( ref : HTMLElement | null ) => void
41
36
onInputRefChanged ?: ( ref : React . RefObject < HTMLInputElement > ) => void
42
37
textInputProps ?: Partial < Omit < TextInputProps , 'onChange' > >
43
38
inputRef ?: React . RefObject < HTMLInputElement >
@@ -58,7 +53,6 @@ export function FilteredActionList({
58
53
filterValue : externalFilterValue ,
59
54
loadingType = FilteredActionListLoadingTypes . bodySpinner ,
60
55
onFilterChange,
61
- onListContainerRefChanged,
62
56
onInputRefChanged,
63
57
items,
64
58
textInputProps,
@@ -68,6 +62,7 @@ export function FilteredActionList({
68
62
showItemDividers,
69
63
message,
70
64
className,
65
+ selectionVariant,
71
66
announcementsEnabled = true ,
72
67
fullScreenOnNarrow,
73
68
...listProps
@@ -82,72 +77,66 @@ export function FilteredActionList({
82
77
[ onFilterChange , setInternalFilterValue ] ,
83
78
)
84
79
80
+ const [ enableAnnouncements , setEnableAnnouncements ] = useState ( false )
81
+ const [ selectedItems , setSelectedItems ] = useState < ( string | number | undefined ) [ ] > ( [ ] )
82
+
85
83
const scrollContainerRef = useRef < HTMLDivElement > ( null )
86
84
const inputRef = useProvidedRefOrCreate < HTMLInputElement > ( providedInputRef )
87
- const [ listContainerElement , setListContainerElement ] = useState < HTMLUListElement | null > ( null )
88
- const activeDescendantRef = useRef < HTMLElement > ( )
85
+ const listRef = useRef < HTMLUListElement > ( null )
89
86
const listId = useId ( )
90
87
const inputDescriptionTextId = useId ( )
91
- const onInputKeyPress : KeyboardEventHandler = useCallback (
92
- event => {
93
- if ( event . key === 'Enter' && activeDescendantRef . current ) {
94
- event . preventDefault ( )
95
- event . nativeEvent . stopImmediatePropagation ( )
88
+ const keydownListener = useCallback (
89
+ ( event : React . KeyboardEvent < HTMLDivElement > ) => {
90
+ if ( event . key === 'ArrowDown' ) {
91
+ if ( listRef . current ) {
92
+ const firstSelectedItem = listRef . current . querySelector ( '[role="option"]' ) as HTMLElement | undefined
93
+ firstSelectedItem ?. focus ( )
96
94
97
- // Forward Enter key press to active descendant so that item gets activated
98
- const activeDescendantEvent = new KeyboardEvent ( event . type , event . nativeEvent )
99
- activeDescendantRef . current . dispatchEvent ( activeDescendantEvent )
100
- }
101
- } ,
102
- [ activeDescendantRef ] ,
103
- )
95
+ event . preventDefault ( )
96
+ }
97
+ } else if ( event . key === 'Enter' ) {
98
+ let firstItem
99
+ // If there are groups, it's not guaranteed that the first item is the actual first item in the first -
100
+ // as groups are rendered in the order of the groupId provided
101
+ if ( groupMetadata ) {
102
+ const firstGroup = groupMetadata [ 0 ] . groupId
103
+ firstItem = items . filter ( item => item . groupId === firstGroup ) [ 0 ]
104
+ } else {
105
+ firstItem = items [ 0 ]
106
+ }
104
107
105
- const listContainerRefCallback = useCallback (
106
- ( node : HTMLUListElement | null ) => {
107
- setListContainerElement ( node )
108
- onListContainerRefChanged ?.( node )
108
+ if ( firstItem . onAction ) {
109
+ firstItem . onAction ( firstItem , event )
110
+ event . preventDefault ( )
111
+ }
112
+ }
109
113
} ,
110
- [ onListContainerRefChanged ] ,
114
+ [ items , groupMetadata ] ,
111
115
)
112
116
113
117
useEffect ( ( ) => {
114
118
onInputRefChanged ?.( inputRef )
115
119
} , [ inputRef , onInputRefChanged ] )
116
120
117
- useFocusZone (
118
- {
119
- containerRef : { current : listContainerElement } ,
120
- bindKeys : FocusKeys . ArrowVertical | FocusKeys . PageUpDown ,
121
- focusOutBehavior : 'wrap' ,
122
- focusableElementFilter : element => {
123
- return ! ( element instanceof HTMLInputElement )
124
- } ,
125
- activeDescendantFocus : inputRef ,
126
- onActiveDescendantChanged : ( current , previous , directlyActivated ) => {
127
- activeDescendantRef . current = current
128
-
129
- if ( current && scrollContainerRef . current && directlyActivated ) {
130
- scrollIntoView ( current , scrollContainerRef . current , menuScrollMargins )
131
- }
132
- } ,
133
- } ,
134
- [
135
- // List container isn't in the DOM while loading. Need to re-bind focus zone when it changes.
136
- listContainerElement ,
137
- ] ,
138
- )
139
-
140
121
useEffect ( ( ) => {
141
- // if items changed, we want to instantly move active descendant into view
142
- if ( activeDescendantRef . current && scrollContainerRef . current ) {
143
- scrollIntoView ( activeDescendantRef . current , scrollContainerRef . current , {
144
- ...menuScrollMargins ,
145
- behavior : 'auto' ,
146
- } )
122
+ if ( items . length === 0 ) {
123
+ inputRef . current ?. focus ( )
124
+ } else {
125
+ const itemIds = items . filter ( item => item . selected ) . map ( item => item . id )
126
+ const removedItem = selectedItems . find ( item => ! itemIds . includes ( item ) )
127
+ if ( removedItem && document . activeElement !== inputRef . current ) {
128
+ const list = listRef . current
129
+ if ( list ) {
130
+ const firstSelectedItem = list . querySelector ( '[role="option"]' ) as HTMLElement
131
+ firstSelectedItem . focus ( )
132
+ }
133
+ }
147
134
}
148
- } , [ items ] )
135
+ } , [ items , inputRef , selectedItems ] )
149
136
150
- useAnnouncements ( items , { current : listContainerElement } , inputRef , announcementsEnabled , loading )
137
+ useEffect ( ( ) => {
138
+ setEnableAnnouncements ( announcementsEnabled )
139
+ } , [ announcementsEnabled ] )
151
140
useScrollFlash ( scrollContainerRef )
152
141
153
142
function getItemListForEachGroup ( groupId : string ) {
@@ -170,36 +159,40 @@ export function FilteredActionList({
170
159
}
171
160
172
161
return (
173
- < ActionList
174
- ref = { listContainerRefCallback }
175
- showDividers = { showItemDividers }
176
- { ...listProps }
177
- role = "listbox"
178
- id = { listId }
179
- sx = { { flexGrow : 1 } }
162
+ < ActionListContainerContext . Provider
163
+ value = { {
164
+ container : 'FilteredActionList' ,
165
+ listRole : 'listbox' ,
166
+ selectionAttribute : 'aria-selected' ,
167
+ selectionVariant,
168
+ enableFocusZone : true ,
169
+ } }
180
170
>
181
- { groupMetadata ?. length
182
- ? groupMetadata . map ( ( group , index ) => {
183
- return (
184
- < ActionList . Group key = { index } >
185
- < ActionList . GroupHeading variant = { group . header ?. variant ? group . header . variant : undefined } >
186
- { group . header ?. title ? group . header . title : `Group ${ group . groupId } ` }
187
- </ ActionList . GroupHeading >
188
- { getItemListForEachGroup ( group . groupId ) . map ( ( { key : itemKey , ...item } , index ) => {
189
- const key = itemKey ?? item . id ?. toString ( ) ?? index . toString ( )
190
- return < MappedActionListItem key = { key } { ...item } renderItem = { listProps . renderItem } />
191
- } ) }
192
- </ ActionList . Group >
193
- )
194
- } )
195
- : items . map ( ( { key : itemKey , ...item } , index ) => {
196
- const key = itemKey ?? item . id ?. toString ( ) ?? index . toString ( )
197
- return < MappedActionListItem key = { key } { ...item } renderItem = { listProps . renderItem } />
198
- } ) }
199
- </ ActionList >
171
+ < ActionList ref = { listRef } showDividers = { showItemDividers } { ...listProps } id = { listId } sx = { { flexGrow : 1 } } >
172
+ { groupMetadata ?. length
173
+ ? groupMetadata . map ( ( group , index ) => {
174
+ return (
175
+ < ActionList . Group key = { index } >
176
+ < ActionList . GroupHeading variant = { group . header ?. variant ? group . header . variant : undefined } >
177
+ { group . header ?. title ? group . header . title : `Group ${ group . groupId } ` }
178
+ </ ActionList . GroupHeading >
179
+ { getItemListForEachGroup ( group . groupId ) . map ( ( { key : itemKey , ...item } , index ) => {
180
+ const key = itemKey ?? item . id ?. toString ( ) ?? index . toString ( )
181
+ return < MappedActionListItem key = { key } { ...item } renderItem = { listProps . renderItem } />
182
+ } ) }
183
+ </ ActionList . Group >
184
+ )
185
+ } )
186
+ : items . map ( ( { key : itemKey , ...item } , index ) => {
187
+ const key = itemKey ?? item . id ?. toString ( ) ?? index . toString ( )
188
+ return < MappedActionListItem key = { key } { ...item } renderItem = { listProps . renderItem } />
189
+ } ) }
190
+ </ ActionList >
191
+ </ ActionListContainerContext . Provider >
200
192
)
201
193
}
202
194
195
+ useAnnouncements ( items , listRef , inputRef , enableAnnouncements )
203
196
return (
204
197
< Box
205
198
display = "flex"
@@ -217,7 +210,7 @@ export function FilteredActionList({
217
210
color = "fg.default"
218
211
value = { filterValue }
219
212
onChange = { onInputChange }
220
- onKeyPress = { onInputKeyPress }
213
+ onKeyDown = { keydownListener }
221
214
placeholder = { placeholderText }
222
215
role = "combobox"
223
216
aria-expanded = "true"
0 commit comments