Skip to content

Commit d165d3f

Browse files
committed
fix: incorporated accessibility feedback
1 parent ef8ac84 commit d165d3f

File tree

3 files changed

+230
-175
lines changed

3 files changed

+230
-175
lines changed

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

Lines changed: 81 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,56 @@ export const FieldBuilderExample: React.FunctionComponent = () => {
1616
]);
1717

1818
// Handle adding a new contact row
19-
const handleAddContact = () => {
20-
setContacts([ ...contacts, { name: '', email: '' } ]);
19+
const handleAddContact = (event: React.MouseEvent) => {
20+
// eslint-disable-next-line no-console
21+
console.log('Add button clicked:', event.currentTarget);
22+
const newContacts = [ ...contacts, { name: '', email: '' } ];
23+
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);
2133
};
2234

2335
// Handle removing a contact row
24-
const handleRemoveContact = (index: number) => {
25-
setContacts(contacts.filter((_, i) => i !== index));
36+
const handleRemoveContact = (event: React.MouseEvent, index: number) => {
37+
// eslint-disable-next-line no-console
38+
console.log('Remove button clicked:', event.currentTarget, 'for index:', index);
39+
const newContacts = contacts.filter((_, i) => i !== index);
40+
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);
2669
};
2770

2871
// Handle updating contact data
@@ -32,6 +75,29 @@ export const FieldBuilderExample: React.FunctionComponent = () => {
3275
setContacts(updatedContacts);
3376
};
3477

78+
// Custom announcement for adding rows
79+
const customAddAnnouncement = (rowNumber: number, rowGroupLabelPrefix: string) => `New ${rowGroupLabelPrefix.toLowerCase()} ${rowNumber} added.`;
80+
81+
// Custom announcement for removing rows
82+
const customRemoveAnnouncement = (rowNumber: number, rowGroupLabelPrefix: string) => {
83+
const removedIndex = rowNumber - 1;
84+
const removedContact = contacts[removedIndex];
85+
if (removedContact?.name) {
86+
return `Removed ${rowGroupLabelPrefix.toLowerCase()} ${removedContact.name}.`;
87+
}
88+
return `${rowGroupLabelPrefix} ${rowNumber} removed.`;
89+
};
90+
91+
// Custom aria-label for remove buttons
92+
const customRemoveAriaLabel = (rowNumber: number, rowGroupLabelPrefix: string) => {
93+
const contactIndex = rowNumber - 1;
94+
const contact = contacts[contactIndex];
95+
if (contact?.name) {
96+
return `Remove ${rowGroupLabelPrefix.toLowerCase()} ${contact.name}`;
97+
}
98+
return `Remove ${rowGroupLabelPrefix.toLowerCase()} in row ${rowNumber}`;
99+
};
100+
35101
return (
36102
<Form>
37103
<FieldBuilder
@@ -43,28 +109,29 @@ export const FieldBuilderExample: React.FunctionComponent = () => {
43109
rowCount={contacts.length}
44110
onAddRow={handleAddContact}
45111
onRemoveRow={handleRemoveContact}
46-
addButtonProps={{
47-
children: 'Add team member'
48-
}}
112+
onAddRowAnnouncement={customAddAnnouncement}
113+
onRemoveRowAnnouncement={customRemoveAnnouncement}
114+
removeButtonAriaLabel={customRemoveAriaLabel}
115+
addButtonContent="Add team member"
49116
>
50-
{({ rowIndex, focusRef, firstColumnAriaLabelledBy, secondColumnAriaLabelledBy }) => [
117+
{({ focusRef, firstColumnAriaLabel, secondColumnAriaLabel }, index) => [
51118
<TextInput
52119
key="name"
53120
ref={focusRef}
54121
type="text"
55-
value={contacts[rowIndex]?.name || ''}
122+
value={contacts[index]?.name || ''}
56123
placeholder="Enter full name"
57-
onChange={(_event, value) => handleContactChange(rowIndex, 'name', value)}
58-
aria-labelledby={firstColumnAriaLabelledBy}
124+
onChange={(_event, value) => handleContactChange(index, 'name', value)}
125+
aria-label={firstColumnAriaLabel}
59126
isRequired
60127
/>,
61128
<TextInput
62129
key="email"
63130
type="email"
64-
value={contacts[rowIndex]?.email || ''}
131+
value={contacts[index]?.email || ''}
65132
placeholder="name@example.com"
66-
onChange={(_event, value) => handleContactChange(rowIndex, 'email', value)}
67-
aria-labelledby={secondColumnAriaLabelledBy}
133+
onChange={(_event, value) => handleContactChange(index, 'email', value)}
134+
aria-label={secondColumnAriaLabel}
68135
isRequired
69136
/>
70137
]}

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

Lines changed: 81 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,56 @@ export const FieldBuilderSelectExample: React.FunctionComponent = () => {
1717
]);
1818

1919
// Handle adding a new team member row
20-
const handleAddTeamMember = () => {
21-
setTeamMembers([ ...teamMembers, { department: '', role: '' } ]);
20+
const handleAddTeamMember = (event: React.MouseEvent) => {
21+
// eslint-disable-next-line no-console
22+
console.log('Add button clicked:', event.currentTarget);
23+
const newTeamMembers = [ ...teamMembers, { department: '', role: '' } ];
24+
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);
2234
};
2335

2436
// Handle removing a team member row
25-
const handleRemoveTeamMember = (index: number) => {
26-
setTeamMembers(teamMembers.filter((_, i) => i !== index));
37+
const handleRemoveTeamMember = (event: React.MouseEvent, index: number) => {
38+
// eslint-disable-next-line no-console
39+
console.log('Remove button clicked:', event.currentTarget, 'for index:', index);
40+
const newTeamMembers = teamMembers.filter((_, i) => i !== index);
41+
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);
2770
};
2871

2972
// Handle updating team member data
@@ -33,6 +76,29 @@ export const FieldBuilderSelectExample: React.FunctionComponent = () => {
3376
setTeamMembers(updatedTeamMembers);
3477
};
3578

79+
// Custom announcement for adding rows
80+
const customAddAnnouncement = (rowNumber: number, rowGroupLabelPrefix: string) => `New ${rowGroupLabelPrefix.toLowerCase()} ${rowNumber} added.`;
81+
82+
// Custom announcement for removing rows
83+
const customRemoveAnnouncement = (rowNumber: number, rowGroupLabelPrefix: string) => {
84+
const removedIndex = rowNumber - 1;
85+
const removedTeamMember = teamMembers[removedIndex];
86+
if (removedTeamMember?.department && removedTeamMember?.role) {
87+
return `Removed ${rowGroupLabelPrefix.toLowerCase()} ${removedTeamMember.role} from ${removedTeamMember.department}.`;
88+
}
89+
return `${rowGroupLabelPrefix} ${rowNumber} removed.`;
90+
};
91+
92+
// Custom aria-label for remove buttons
93+
const customRemoveAriaLabel = (rowNumber: number, rowGroupLabelPrefix: string) => {
94+
const teamMemberIndex = rowNumber - 1;
95+
const teamMember = teamMembers[teamMemberIndex];
96+
if (teamMember?.department && teamMember?.role) {
97+
return `Remove ${rowGroupLabelPrefix.toLowerCase()} ${teamMember.role} from ${teamMember.department}`;
98+
}
99+
return `Remove ${rowGroupLabelPrefix.toLowerCase()} in row ${rowNumber}`;
100+
};
101+
36102
// Create a ref callback that works with FormSelect
37103
const createFormSelectRef = (focusRef: (element: HTMLElement | null) => void) =>
38104
(instance: React.ComponentRef<typeof FormSelect> | HTMLElement | null) => {
@@ -74,18 +140,19 @@ export const FieldBuilderSelectExample: React.FunctionComponent = () => {
74140
rowCount={teamMembers.length}
75141
onAddRow={handleAddTeamMember}
76142
onRemoveRow={handleRemoveTeamMember}
143+
onAddRowAnnouncement={customAddAnnouncement}
144+
onRemoveRowAnnouncement={customRemoveAnnouncement}
145+
removeButtonAriaLabel={customRemoveAriaLabel}
77146
rowGroupLabelPrefix="Team member"
78-
addButtonProps={{
79-
children: 'Add team member'
80-
}}
147+
addButtonContent="Add team member"
81148
>
82-
{({ rowIndex, focusRef, firstColumnAriaLabelledBy, secondColumnAriaLabelledBy }) => [
149+
{({ focusRef, firstColumnAriaLabel, secondColumnAriaLabel }, index) => [
83150
<FormSelect
84151
key="department"
85152
ref={createFormSelectRef(focusRef)}
86-
value={teamMembers[rowIndex]?.department || ''}
87-
onChange={(event, value) => handleTeamMemberChange(rowIndex, 'department', value)}
88-
aria-labelledby={firstColumnAriaLabelledBy}
153+
value={teamMembers[index]?.department || ''}
154+
onChange={(event, value) => handleTeamMemberChange(index, 'department', value)}
155+
aria-label={firstColumnAriaLabel}
89156
isRequired
90157
>
91158
{departmentOptions.map((option, optionIndex) => (
@@ -99,9 +166,9 @@ export const FieldBuilderSelectExample: React.FunctionComponent = () => {
99166
</FormSelect>,
100167
<FormSelect
101168
key="role"
102-
value={teamMembers[rowIndex]?.role || ''}
103-
onChange={(event, value) => handleTeamMemberChange(rowIndex, 'role', value)}
104-
aria-labelledby={secondColumnAriaLabelledBy}
169+
value={teamMembers[index]?.role || ''}
170+
onChange={(event, value) => handleTeamMemberChange(index, 'role', value)}
171+
aria-label={secondColumnAriaLabel}
105172
isRequired
106173
>
107174
{roleOptions.map((option, optionIndex) => (

0 commit comments

Comments
 (0)