Skip to content

Commit 77697da

Browse files
committed
fix: uses Select instead of FormSelect and focus management fixed
1 parent 7deaff0 commit 77697da

File tree

2 files changed

+159
-71
lines changed

2 files changed

+159
-71
lines changed

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

Lines changed: 125 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import React, { useState } from 'react';
22
import {
33
Form,
4-
FormSelect,
5-
FormSelectOption,
4+
Select,
5+
SelectOption,
6+
SelectList,
7+
MenuToggle,
8+
MenuToggleElement,
69
} from '@patternfly/react-core';
710
import { FieldBuilder } from '@patternfly/react-component-groups/dist/dynamic/FieldBuilder';
811

@@ -16,12 +19,19 @@ export const FieldBuilderSelectExample: React.FunctionComponent = () => {
1619
{ department: '', role: '' }
1720
]);
1821

22+
// State for managing which select dropdowns are open
23+
const [ departmentOpenStates, setDepartmentOpenStates ] = useState<boolean[]>([ false ]);
24+
const [ roleOpenStates, setRoleOpenStates ] = useState<boolean[]>([ false ]);
25+
1926
// Handle adding a new team member row
2027
const handleAddTeamMember = (event: React.MouseEvent) => {
2128
// eslint-disable-next-line no-console
2229
console.log('Add button clicked:', event.currentTarget);
2330
const newTeamMembers = [ ...teamMembers, { department: '', role: '' } ];
2431
setTeamMembers(newTeamMembers);
32+
// Add new open states for the selects
33+
setDepartmentOpenStates([ ...departmentOpenStates, false ]);
34+
setRoleOpenStates([ ...roleOpenStates, false ]);
2535
};
2636

2737
// Handle removing a team member row
@@ -30,6 +40,9 @@ export const FieldBuilderSelectExample: React.FunctionComponent = () => {
3040
console.log('Remove button clicked:', event.currentTarget, 'for index:', index);
3141
const newTeamMembers = teamMembers.filter((_, i) => i !== index);
3242
setTeamMembers(newTeamMembers);
43+
// Remove corresponding open states
44+
setDepartmentOpenStates(departmentOpenStates.filter((_, i) => i !== index));
45+
setRoleOpenStates(roleOpenStates.filter((_, i) => i !== index));
3346
};
3447

3548
// Handle updating team member data
@@ -39,6 +52,36 @@ export const FieldBuilderSelectExample: React.FunctionComponent = () => {
3952
setTeamMembers(updatedTeamMembers);
4053
};
4154

55+
// Handle department select open/close
56+
const handleDepartmentToggle = (index: number) => {
57+
const newOpenStates = [ ...departmentOpenStates ];
58+
newOpenStates[index] = !newOpenStates[index];
59+
setDepartmentOpenStates(newOpenStates);
60+
};
61+
62+
// Handle role select open/close
63+
const handleRoleToggle = (index: number) => {
64+
const newOpenStates = [ ...roleOpenStates ];
65+
newOpenStates[index] = !newOpenStates[index];
66+
setRoleOpenStates(newOpenStates);
67+
};
68+
69+
// Handle department selection
70+
const handleDepartmentSelect = (index: number, _event: React.MouseEvent<Element, MouseEvent> | undefined, value: string | number | undefined) => {
71+
handleTeamMemberChange(index, 'department', value as string);
72+
const newOpenStates = [ ...departmentOpenStates ];
73+
newOpenStates[index] = false;
74+
setDepartmentOpenStates(newOpenStates);
75+
};
76+
77+
// Handle role selection
78+
const handleRoleSelect = (index: number, _event: React.MouseEvent<Element, MouseEvent> | undefined, value: string | number | undefined) => {
79+
handleTeamMemberChange(index, 'role', value as string);
80+
const newOpenStates = [ ...roleOpenStates ];
81+
newOpenStates[index] = false;
82+
setRoleOpenStates(newOpenStates);
83+
};
84+
4285
// Custom announcement for adding rows
4386
const customAddAnnouncement = (rowNumber: number, rowGroupLabelPrefix: string) => `New ${rowGroupLabelPrefix.toLowerCase()} ${rowNumber} added.`;
4487

@@ -62,18 +105,6 @@ export const FieldBuilderSelectExample: React.FunctionComponent = () => {
62105
return `Remove ${rowGroupLabelPrefix.toLowerCase()} in row ${rowNumber}`;
63106
};
64107

65-
// Create a ref callback that works with FormSelect
66-
const createFormSelectRef = (focusRef: (element: HTMLElement | null) => void) =>
67-
(instance: React.ComponentRef<typeof FormSelect> | HTMLElement | null) => {
68-
if (instance) {
69-
// Get the underlying DOM element from the FormSelect instance
70-
const domElement = (instance as any)?.ref?.current || instance;
71-
if (domElement instanceof HTMLElement) {
72-
focusRef(domElement);
73-
}
74-
}
75-
};
76-
77108
const departmentOptions = [
78109
{ label: 'Choose a department', value: '', disabled: true },
79110
{ label: 'Engineering', value: 'engineering' },
@@ -110,39 +141,90 @@ export const FieldBuilderSelectExample: React.FunctionComponent = () => {
110141
addButtonContent="Add team member"
111142
>
112143
{({ focusRef, firstColumnAriaLabel, secondColumnAriaLabel }, index) => [
113-
<FormSelect
144+
<Select
114145
key="department"
115-
ref={createFormSelectRef(focusRef)}
116-
value={teamMembers[index]?.department || ''}
117-
onChange={(event, value) => handleTeamMemberChange(index, 'department', value)}
118-
aria-label={firstColumnAriaLabel}
119-
isRequired
146+
id={`department-select-${index}`}
147+
isOpen={departmentOpenStates[index] || false}
148+
selected={teamMembers[index]?.department || ''}
149+
onSelect={(event, value) => handleDepartmentSelect(index, event, value)}
150+
onOpenChange={(isOpen) => {
151+
const newOpenStates = [ ...departmentOpenStates ];
152+
newOpenStates[index] = isOpen;
153+
setDepartmentOpenStates(newOpenStates);
154+
}}
155+
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
156+
<MenuToggle
157+
ref={(element) => {
158+
// Handle both the toggle ref and focus ref
159+
if (typeof toggleRef === 'function') {
160+
toggleRef(element);
161+
} else if (toggleRef && 'current' in toggleRef && toggleRef.current !== element) {
162+
(toggleRef as React.MutableRefObject<MenuToggleElement | null>).current = element;
163+
}
164+
focusRef(element);
165+
}}
166+
onClick={() => handleDepartmentToggle(index)}
167+
isExpanded={departmentOpenStates[index] || false}
168+
aria-label={firstColumnAriaLabel}
169+
style={{ width: '100%' }}
170+
>
171+
{teamMembers[index]?.department ?
172+
departmentOptions.find(opt => opt.value === teamMembers[index]?.department)?.label || 'Choose a department'
173+
: 'Choose a department'}
174+
</MenuToggle>
175+
)}
176+
shouldFocusToggleOnSelect
120177
>
121-
{departmentOptions.map((option, optionIndex) => (
122-
<FormSelectOption
123-
key={optionIndex}
124-
value={option.value}
125-
label={option.label}
126-
isDisabled={option.disabled}
127-
/>
128-
))}
129-
</FormSelect>,
130-
<FormSelect
178+
<SelectList>
179+
{departmentOptions.map((option, optionIndex) => (
180+
<SelectOption
181+
key={optionIndex}
182+
value={option.value}
183+
isDisabled={option.disabled}
184+
>
185+
{option.label}
186+
</SelectOption>
187+
))}
188+
</SelectList>
189+
</Select>,
190+
<Select
131191
key="role"
132-
value={teamMembers[index]?.role || ''}
133-
onChange={(event, value) => handleTeamMemberChange(index, 'role', value)}
134-
aria-label={secondColumnAriaLabel}
135-
isRequired
192+
id={`role-select-${index}`}
193+
isOpen={roleOpenStates[index] || false}
194+
selected={teamMembers[index]?.role || ''}
195+
onSelect={(event, value) => handleRoleSelect(index, event, value)}
196+
onOpenChange={(isOpen) => {
197+
const newOpenStates = [ ...roleOpenStates ];
198+
newOpenStates[index] = isOpen;
199+
setRoleOpenStates(newOpenStates);
200+
}}
201+
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
202+
<MenuToggle
203+
ref={toggleRef}
204+
onClick={() => handleRoleToggle(index)}
205+
isExpanded={roleOpenStates[index] || false}
206+
aria-label={secondColumnAriaLabel}
207+
style={{ width: '100%' }}
208+
>
209+
{teamMembers[index]?.role ?
210+
roleOptions.find(opt => opt.value === teamMembers[index]?.role)?.label || 'Choose a role'
211+
: 'Choose a role'}
212+
</MenuToggle>
213+
)}
214+
shouldFocusToggleOnSelect
136215
>
137-
{roleOptions.map((option, optionIndex) => (
138-
<FormSelectOption
139-
key={optionIndex}
140-
value={option.value}
141-
label={option.label}
142-
isDisabled={option.disabled}
143-
/>
144-
))}
145-
</FormSelect>
216+
<SelectList>
217+
{roleOptions.map((option, optionIndex) => (
218+
<SelectOption
219+
key={optionIndex}
220+
value={option.value}
221+
isDisabled={option.disabled}
222+
>
223+
{option.label}
224+
</SelectOption>
225+
))}
226+
</SelectList>
227+
</Select>
146228
]}
147229
</FieldBuilder>
148230
</Form>

packages/module/src/FieldBuilder/FieldBuilder.tsx

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -137,37 +137,43 @@ export const FieldBuilder: FunctionComponent<FieldBuilderProps> = ({
137137

138138
if (rowCount > previousRowCount) {
139139
// 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-
}
140+
// Use setTimeout to ensure DOM is fully rendered for complex components like Select
141+
setTimeout(() => {
142+
const newRowIndex = rowCount - 1;
143+
const newRowFirstElement = focusableElementsRef.current.get(newRowIndex);
144+
if (newRowFirstElement) {
145+
newRowFirstElement.focus();
146+
}
147+
}, 0);
145148
} else if (rowCount < previousRowCount && lastRemovedIndexRef.current !== null) {
146149
// 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();
150+
// Use setTimeout to ensure DOM is fully updated after row removal
151+
setTimeout(() => {
152+
const removedIndex = lastRemovedIndexRef.current!;
153+
154+
if (rowCount === 0) {
155+
// No rows left - focus the add button
156+
if (addButtonRef.current) {
157+
addButtonRef.current.focus();
158+
}
159+
} else if (removedIndex >= rowCount) {
160+
// Removed the last row - focus the new last row's first element
161+
const newLastRowIndex = rowCount - 1;
162+
const newLastRowFirstElement = focusableElementsRef.current.get(newLastRowIndex);
163+
if (newLastRowFirstElement) {
164+
newLastRowFirstElement.focus();
165+
}
166+
} else {
167+
// Removed a middle row - focus the first element of the row that took its place
168+
const sameIndexFirstElement = focusableElementsRef.current.get(removedIndex);
169+
if (sameIndexFirstElement) {
170+
sameIndexFirstElement.focus();
171+
}
160172
}
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;
173+
174+
// Reset the removed index tracker
175+
lastRemovedIndexRef.current = null;
176+
}, 0);
171177
}
172178

173179
// Update the previous row count

0 commit comments

Comments
 (0)