1- import React , { FunctionComponent , Children , useRef , useCallback , useState } from 'react' ;
1+ import React , { FunctionComponent , Children , useRef , useCallback , useState , useEffect } from 'react' ;
22import {
33 Button ,
44 ButtonProps ,
55 FormGroup ,
66 type FormGroupProps ,
77 Flex ,
88 FlexItem ,
9- Grid ,
10- GridItem ,
119} from '@patternfly/react-core' ;
10+ import { Table , Tbody , Td , Th , Tr , Thead } from '@patternfly/react-table' ;
1211import { PlusCircleIcon , MinusCircleIcon } from '@patternfly/react-icons' ;
1312
1413/**
@@ -116,6 +115,12 @@ export const FieldBuilder: FunctionComponent<FieldBuilderProps> = ({
116115 const focusableElementsRef = useRef < Map < number , HTMLElement > > ( new Map ( ) ) ;
117116 // State for ARIA live region announcements
118117 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 ) ;
119124
120125 // Function to announce changes to screen readers
121126 const announceChange = useCallback ( ( message : string ) => {
@@ -126,6 +131,49 @@ export const FieldBuilder: FunctionComponent<FieldBuilderProps> = ({
126131 } , 1000 ) ;
127132 } , [ ] ) ;
128133
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+
129177 // Create ref callback for focusable elements
130178 const createFocusRef = useCallback ( ( rowIndex : number ) =>
131179 ( element : HTMLElement | null ) => {
@@ -144,10 +192,13 @@ export const FieldBuilder: FunctionComponent<FieldBuilderProps> = ({
144192 announceChange ( announcementMessage ) ;
145193 } , [ onAddRow , announceChange , rowGroupLabelPrefix , rowCount , onAddRowAnnouncement ] ) ;
146194
147- // Enhanced onRemoveRow with announcements
195+ // Enhanced onRemoveRow with announcements and focus tracking
148196 const handleRemoveRow = useCallback ( ( event : React . MouseEvent , index : number ) => {
149197 const rowNumber = index + 1 ;
150198
199+ // Track which row is being removed for focus management
200+ lastRemovedIndexRef . current = index ;
201+
151202 onRemoveRow ( event , index ) ;
152203
153204 // Announce the removal
@@ -183,33 +234,35 @@ export const FieldBuilder: FunctionComponent<FieldBuilderProps> = ({
183234 }
184235 }
185236
186- // Determine span based on number of children
187- const cellSpan = cells . length === 1 ? 10 : 5 ;
188-
189237 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" >
204257 < Button
205258 variant = "plain"
206259 aria-label = { removeButtonAriaLabel ? removeButtonAriaLabel ( rowNumber , rowGroupLabelPrefix ) : `Remove ${ rowGroupLabelPrefix . toLowerCase ( ) } ${ rowNumber } ` }
207260 onClick = { ( event ) => handleRemoveRow ( event , index ) }
208261 icon = { < MinusCircleIcon /> }
209262 { ...removeButtonProps }
210263 />
211- </ GridItem >
212- </ Grid >
264+ </ Td >
265+ </ Tr >
213266 ) ;
214267 } ) ;
215268 } ;
@@ -221,41 +274,50 @@ export const FieldBuilder: FunctionComponent<FieldBuilderProps> = ({
221274 { /* ARIA Live Region for announcing dynamic changes */ }
222275 < div
223276 className = "pf-v6-screen-reader"
224- aria-live = "polite"
225- aria-atomic = "true"
226- role = "status"
277+ aria-live = "polite"
227278 >
228279 { liveRegionMessage }
229280 </ div >
230281
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 >
251312
252313 { /* The "Add" button for creating a new row */ }
253- < FlexItem className = "pf-v6-u-mt-md " >
314+ < FlexItem className = "pf-v6-u-mt-sm " >
254315 < Button
316+ ref = { addButtonRef }
255317 variant = "link"
256- isInline
257318 onClick = { handleAddRow }
258319 icon = { < PlusCircleIcon /> }
320+ aria-label = { `Add ${ rowGroupLabelPrefix . toLowerCase ( ) } ` }
259321 { ...addButtonProps }
260322 >
261323 { addButtonContent || 'Add another' }
0 commit comments