1
- import React , { FunctionComponent , Children , useRef , useCallback , useState } from 'react' ;
1
+ import React , { FunctionComponent , Children , useRef , useCallback , useState , useEffect } from 'react' ;
2
2
import {
3
3
Button ,
4
4
ButtonProps ,
5
5
FormGroup ,
6
6
type FormGroupProps ,
7
7
Flex ,
8
8
FlexItem ,
9
- Grid ,
10
- GridItem ,
11
9
} from '@patternfly/react-core' ;
10
+ import { Table , Tbody , Td , Th , Tr , Thead } from '@patternfly/react-table' ;
12
11
import { PlusCircleIcon , MinusCircleIcon } from '@patternfly/react-icons' ;
13
12
14
13
/**
@@ -116,6 +115,12 @@ export const FieldBuilder: FunctionComponent<FieldBuilderProps> = ({
116
115
const focusableElementsRef = useRef < Map < number , HTMLElement > > ( new Map ( ) ) ;
117
116
// State for ARIA live region announcements
118
117
const [ liveRegionMessage , setLiveRegionMessage ] = useState < string > ( '' ) ;
118
+ // Track previous row count for focus management
119
+ const previousRowCountRef = useRef < number > ( rowCount ) ;
120
+ // Track the last removed row index for focus management
121
+ const lastRemovedIndexRef = useRef < number | null > ( null ) ;
122
+ // Reference to the add button for focus management
123
+ const addButtonRef = useRef < HTMLButtonElement > ( null ) ;
119
124
120
125
// Function to announce changes to screen readers
121
126
const announceChange = useCallback ( ( message : string ) => {
@@ -126,6 +131,49 @@ export const FieldBuilder: FunctionComponent<FieldBuilderProps> = ({
126
131
} , 1000 ) ;
127
132
} , [ ] ) ;
128
133
134
+ // Focus management effect - runs when rowCount changes
135
+ useEffect ( ( ) => {
136
+ const previousRowCount = previousRowCountRef . current ;
137
+
138
+ if ( rowCount > previousRowCount ) {
139
+ // Row was added - focus the first input of the new row
140
+ const newRowIndex = rowCount - 1 ;
141
+ const newRowFirstElement = focusableElementsRef . current . get ( newRowIndex ) ;
142
+ if ( newRowFirstElement ) {
143
+ newRowFirstElement . focus ( ) ;
144
+ }
145
+ } else if ( rowCount < previousRowCount && lastRemovedIndexRef . current !== null ) {
146
+ // Row was removed - apply smart focus logic
147
+ const removedIndex = lastRemovedIndexRef . current ;
148
+
149
+ if ( rowCount === 0 ) {
150
+ // No rows left - focus the add button
151
+ if ( addButtonRef . current ) {
152
+ addButtonRef . current . focus ( ) ;
153
+ }
154
+ } else if ( removedIndex >= rowCount ) {
155
+ // Removed the last row - focus the new last row's first element
156
+ const newLastRowIndex = rowCount - 1 ;
157
+ const newLastRowFirstElement = focusableElementsRef . current . get ( newLastRowIndex ) ;
158
+ if ( newLastRowFirstElement ) {
159
+ newLastRowFirstElement . focus ( ) ;
160
+ }
161
+ } else {
162
+ // Removed a middle row - focus the first element of the row that took its place
163
+ const sameIndexFirstElement = focusableElementsRef . current . get ( removedIndex ) ;
164
+ if ( sameIndexFirstElement ) {
165
+ sameIndexFirstElement . focus ( ) ;
166
+ }
167
+ }
168
+
169
+ // Reset the removed index tracker
170
+ lastRemovedIndexRef . current = null ;
171
+ }
172
+
173
+ // Update the previous row count
174
+ previousRowCountRef . current = rowCount ;
175
+ } , [ rowCount ] ) ;
176
+
129
177
// Create ref callback for focusable elements
130
178
const createFocusRef = useCallback ( ( rowIndex : number ) =>
131
179
( element : HTMLElement | null ) => {
@@ -144,10 +192,13 @@ export const FieldBuilder: FunctionComponent<FieldBuilderProps> = ({
144
192
announceChange ( announcementMessage ) ;
145
193
} , [ onAddRow , announceChange , rowGroupLabelPrefix , rowCount , onAddRowAnnouncement ] ) ;
146
194
147
- // Enhanced onRemoveRow with announcements
195
+ // Enhanced onRemoveRow with announcements and focus tracking
148
196
const handleRemoveRow = useCallback ( ( event : React . MouseEvent , index : number ) => {
149
197
const rowNumber = index + 1 ;
150
198
199
+ // Track which row is being removed for focus management
200
+ lastRemovedIndexRef . current = index ;
201
+
151
202
onRemoveRow ( event , index ) ;
152
203
153
204
// Announce the removal
@@ -183,33 +234,35 @@ export const FieldBuilder: FunctionComponent<FieldBuilderProps> = ({
183
234
}
184
235
}
185
236
186
- // Determine span based on number of children
187
- const cellSpan = cells . length === 1 ? 10 : 5 ;
188
-
189
237
return (
190
- < Grid
191
- key = { `field-row-${ index } ` }
192
- hasGutter
193
- className = "pf-v6-u-mb-md"
194
- role = "group"
195
- >
196
- { /* Map over the user's components and wrap each one in a GridItem with dynamic spans. */ }
197
- { cells . map ( ( cell , cellIndex ) => (
198
- < GridItem key = { cellIndex } span = { cellSpan } >
199
- { cell }
200
- </ GridItem >
201
- ) ) }
202
- { /* Automatically add the remove button as the last item in the row. */ }
203
- < GridItem span = { 2 } >
238
+ < Tr key = { `field-row-${ index } ` } role = "group" >
239
+ { /* First column cell */ }
240
+ < Td
241
+ dataLabel = { String ( firstColumnLabel ) }
242
+ className = { secondColumnLabel ? "pf-m-width-40" : "pf-m-width-80" }
243
+ >
244
+ { cells [ 0 ] }
245
+ </ Td >
246
+ { /* Second column cell (if two-column layout) */ }
247
+ { secondColumnLabel && (
248
+ < Td
249
+ dataLabel = { String ( secondColumnLabel ) }
250
+ className = "pf-m-width-40"
251
+ >
252
+ { cells [ 1 ] || < div /> }
253
+ </ Td >
254
+ ) }
255
+ { /* Remove button column */ }
256
+ < Td className = "pf-m-width-20" >
204
257
< Button
205
258
variant = "plain"
206
259
aria-label = { removeButtonAriaLabel ? removeButtonAriaLabel ( rowNumber , rowGroupLabelPrefix ) : `Remove ${ rowGroupLabelPrefix . toLowerCase ( ) } ${ rowNumber } ` }
207
260
onClick = { ( event ) => handleRemoveRow ( event , index ) }
208
261
icon = { < MinusCircleIcon /> }
209
262
{ ...removeButtonProps }
210
263
/>
211
- </ GridItem >
212
- </ Grid >
264
+ </ Td >
265
+ </ Tr >
213
266
) ;
214
267
} ) ;
215
268
} ;
@@ -221,41 +274,50 @@ export const FieldBuilder: FunctionComponent<FieldBuilderProps> = ({
221
274
{ /* ARIA Live Region for announcing dynamic changes */ }
222
275
< div
223
276
className = "pf-v6-screen-reader"
224
- aria-live = "polite"
225
- aria-atomic = "true"
226
- role = "status"
277
+ aria-live = "polite"
227
278
>
228
279
{ liveRegionMessage }
229
280
</ div >
230
281
231
- { /* Render the column headers */ }
232
- < Grid hasGutter className = "pf-v6-u-mb-md" >
233
- < GridItem span = { secondColumnLabel ? 5 : 10 } >
234
- < span className = "pf-v6-c-form__label-text" >
235
- { firstColumnLabel }
236
- </ span >
237
- </ GridItem >
238
- { secondColumnLabel && (
239
- < GridItem span = { 5 } >
240
- < span className = "pf-v6-c-form__label-text" >
241
- { secondColumnLabel }
242
- </ span >
243
- </ GridItem >
244
- ) }
245
- { /* Empty GridItem to align with the remove button column */ }
246
- < GridItem span = { 2 } />
247
- </ Grid >
248
-
249
- { /* Render all the dynamic rows of fields */ }
250
- { renderRows ( ) }
282
+ { /* Table layout */ }
283
+ < Table
284
+ aria-label = { `${ rowGroupLabelPrefix } management table` }
285
+ variant = "compact"
286
+ borders = { false }
287
+ style = { {
288
+ '--pf-v6-c-table--cell--PaddingInlineStart' : '0' ,
289
+ '--pf-v6-c-table--cell--first-last-child--PaddingInline' : '0 1rem 0 0' ,
290
+ '--pf-v6-c-table--cell--PaddingBlockStart' : 'var(--pf-t--global--spacer--sm)' ,
291
+ '--pf-v6-c-table--cell--PaddingBlockEnd' : 'var(--pf-t--global--spacer--sm)' ,
292
+ '--pf-v6-c-table__thead--cell--PaddingBlockEnd' : 'var(--pf-t--global--spacer--sm)'
293
+ } as React . CSSProperties }
294
+ >
295
+ < Thead >
296
+ < Tr >
297
+ < Th className = { secondColumnLabel ? "pf-m-width-40" : "pf-m-width-80" } >
298
+ { firstColumnLabel }
299
+ </ Th >
300
+ { secondColumnLabel && (
301
+ < Th className = "pf-m-width-40" >
302
+ { secondColumnLabel }
303
+ </ Th >
304
+ ) }
305
+ < Th screenReaderText = "Actions" className = "pf-m-width-20" />
306
+ </ Tr >
307
+ </ Thead >
308
+ < Tbody >
309
+ { renderRows ( ) }
310
+ </ Tbody >
311
+ </ Table >
251
312
252
313
{ /* The "Add" button for creating a new row */ }
253
- < FlexItem className = "pf-v6-u-mt-md " >
314
+ < FlexItem className = "pf-v6-u-mt-sm " >
254
315
< Button
316
+ ref = { addButtonRef }
255
317
variant = "link"
256
- isInline
257
318
onClick = { handleAddRow }
258
319
icon = { < PlusCircleIcon /> }
320
+ aria-label = { `Add ${ rowGroupLabelPrefix . toLowerCase ( ) } ` }
259
321
{ ...addButtonProps }
260
322
>
261
323
{ addButtonContent || 'Add another' }
0 commit comments