Skip to content

Commit 7deaff0

Browse files
committed
fix: switched from grid to table
1 parent eda5df9 commit 7deaff0

File tree

3 files changed

+111
-123
lines changed

3 files changed

+111
-123
lines changed

packages/module/patternfly-docs/content/extensions/component-groups/examples/FieldBuilder/FieldBuilderExample.tsx

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,6 @@ export const FieldBuilderExample: React.FunctionComponent = () => {
2121
console.log('Add button clicked:', event.currentTarget);
2222
const newContacts = [ ...contacts, { name: '', email: '' } ];
2323
setContacts(newContacts);
24-
25-
// Focus management: focus the first field of the new row
26-
setTimeout(() => {
27-
const newRowNumber = newContacts.length;
28-
const newRowFirstInput = document.querySelector(`input[aria-label*="Row ${newRowNumber}"][aria-label*="Name"]`) as HTMLInputElement;
29-
if (newRowFirstInput) {
30-
newRowFirstInput.focus();
31-
}
32-
}, 100);
3324
};
3425

3526
// Handle removing a contact row
@@ -38,34 +29,6 @@ export const FieldBuilderExample: React.FunctionComponent = () => {
3829
console.log('Remove button clicked:', event.currentTarget, 'for index:', index);
3930
const newContacts = contacts.filter((_, i) => i !== index);
4031
setContacts(newContacts);
41-
42-
// Focus management: avoid focusing on destructive actions
43-
setTimeout(() => {
44-
// If there are still contacts after removal
45-
if (newContacts.length > 0) {
46-
// If we removed the last row, focus the new last row's first input
47-
if (index >= newContacts.length) {
48-
const newLastRowIndex = newContacts.length;
49-
const previousRowFirstInput = document.querySelector(`input[aria-label*="Row ${newLastRowIndex}"][aria-label*="Name"]`) as HTMLInputElement;
50-
if (previousRowFirstInput) {
51-
previousRowFirstInput.focus();
52-
}
53-
} else {
54-
// If we removed a middle row, focus the first input of the row that took its place
55-
const newRowNumber = index + 1;
56-
const sameIndexFirstInput = document.querySelector(`input[aria-label*="Row ${newRowNumber}"][aria-label*="Name"]`) as HTMLInputElement;
57-
if (sameIndexFirstInput) {
58-
sameIndexFirstInput.focus();
59-
}
60-
}
61-
} else {
62-
// If this was the last contact, focus the add button
63-
const addButton = document.querySelector('button[aria-label*="Add"]') as HTMLButtonElement;
64-
if (addButton) {
65-
addButton.focus();
66-
}
67-
}
68-
}, 100);
6932
};
7033

7134
// Handle updating contact data
@@ -112,7 +75,7 @@ export const FieldBuilderExample: React.FunctionComponent = () => {
11275
onAddRowAnnouncement={customAddAnnouncement}
11376
onRemoveRowAnnouncement={customRemoveAnnouncement}
11477
removeButtonAriaLabel={customRemoveAriaLabel}
115-
addButtonContent="Add team member"
78+
addButtonContent="Add contact"
11679
>
11780
{({ focusRef, firstColumnAriaLabel, secondColumnAriaLabel }, index) => [
11881
<TextInput

packages/module/patternfly-docs/content/extensions/component-groups/examples/FieldBuilder/FieldBuilderSelectExample.tsx

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,6 @@ export const FieldBuilderSelectExample: React.FunctionComponent = () => {
2222
console.log('Add button clicked:', event.currentTarget);
2323
const newTeamMembers = [ ...teamMembers, { department: '', role: '' } ];
2424
setTeamMembers(newTeamMembers);
25-
26-
// Focus management: focus the first field of the new row
27-
setTimeout(() => {
28-
const newRowNumber = newTeamMembers.length;
29-
const newRowFirstSelect = document.querySelector(`select[aria-label*="Team member ${newRowNumber}"][aria-label*="Department"]`) as HTMLSelectElement;
30-
if (newRowFirstSelect) {
31-
newRowFirstSelect.focus();
32-
}
33-
}, 100);
3425
};
3526

3627
// Handle removing a team member row
@@ -39,34 +30,6 @@ export const FieldBuilderSelectExample: React.FunctionComponent = () => {
3930
console.log('Remove button clicked:', event.currentTarget, 'for index:', index);
4031
const newTeamMembers = teamMembers.filter((_, i) => i !== index);
4132
setTeamMembers(newTeamMembers);
42-
43-
// Focus management: avoid focusing on destructive actions
44-
setTimeout(() => {
45-
// If there are still team members after removal
46-
if (newTeamMembers.length > 0) {
47-
// If we removed the last row, focus the new last row's first select
48-
if (index >= newTeamMembers.length) {
49-
const newLastRowIndex = newTeamMembers.length;
50-
const previousRowFirstSelect = document.querySelector(`select[aria-label*="Team member ${newLastRowIndex}"][aria-label*="Department"]`) as HTMLSelectElement;
51-
if (previousRowFirstSelect) {
52-
previousRowFirstSelect.focus();
53-
}
54-
} else {
55-
// If we removed a middle row, focus the first select of the row that took its place
56-
const newRowNumber = index + 1;
57-
const sameIndexFirstSelect = document.querySelector(`select[aria-label*="Team member ${newRowNumber}"][aria-label*="Department"]`) as HTMLSelectElement;
58-
if (sameIndexFirstSelect) {
59-
sameIndexFirstSelect.focus();
60-
}
61-
}
62-
} else {
63-
// If this was the last team member, focus the add button
64-
const addButton = document.querySelector('button[aria-label*="Add"]') as HTMLButtonElement;
65-
if (addButton) {
66-
addButton.focus();
67-
}
68-
}
69-
}, 100);
7033
};
7134

7235
// Handle updating team member data

packages/module/src/FieldBuilder/FieldBuilder.tsx

Lines changed: 110 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
import React, { FunctionComponent, Children, useRef, useCallback, useState } from 'react';
1+
import React, { FunctionComponent, Children, useRef, useCallback, useState, useEffect } from 'react';
22
import {
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';
1211
import { 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

Comments
 (0)