-
Notifications
You must be signed in to change notification settings - Fork 30
feat(component): Field builder component #777
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
663c60e
aeb7b7a
eda5df9
7deaff0
77697da
9cbc3a9
aa05ffe
4384ec1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
--- | ||
section: Component groups | ||
subsection: Helpers | ||
id: Field Builder | ||
source: react | ||
propComponents: ['FieldBuilder'] | ||
--- | ||
|
||
import { FunctionComponent, useState } from 'react'; | ||
import { FieldBuilder } from '@patternfly/react-component-groups/dist/dynamic/FieldBuilder'; | ||
import { MinusCircleIcon } from '@patternfly/react-icons'; | ||
|
||
|
||
## Examples | ||
|
||
### Basic Field Builder | ||
|
||
This is a basic field builder! | ||
|
||
```js file="./FieldBuilderExample.tsx" | ||
|
||
``` | ||
|
||
### Field Builder Select | ||
|
||
This is a field builder with Select components! | ||
|
||
```js file="./FieldBuilderSelectExample.tsx" | ||
|
||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import React, { useState } from 'react'; | ||
import { | ||
Form, | ||
TextInput, | ||
} from '@patternfly/react-core'; | ||
import { FieldBuilder } from '@patternfly/react-component-groups/dist/dynamic/FieldBuilder'; | ||
|
||
interface Contact { | ||
name: string; | ||
email: string; | ||
} | ||
|
||
export const FieldBuilderExample: React.FunctionComponent = () => { | ||
const [ contacts, setContacts ] = useState<Contact[]>([ | ||
{ name: '', email: '' } | ||
]); | ||
|
||
// Handle adding a new contact row | ||
const handleAddContact = (event: React.MouseEvent) => { | ||
// eslint-disable-next-line no-console | ||
console.log('Add button clicked:', event.currentTarget); | ||
const newContacts = [ ...contacts, { name: '', email: '' } ]; | ||
setContacts(newContacts); | ||
}; | ||
|
||
// Handle removing a contact row | ||
const handleRemoveContact = (event: React.MouseEvent, index: number) => { | ||
// eslint-disable-next-line no-console | ||
console.log('Remove button clicked:', event.currentTarget, 'for index:', index); | ||
const newContacts = contacts.filter((_, i) => i !== index); | ||
setContacts(newContacts); | ||
}; | ||
|
||
// Handle updating contact data | ||
const handleContactChange = (index: number, field: keyof Contact, value: string) => { | ||
const updatedContacts = [ ...contacts ]; | ||
updatedContacts[index] = { ...updatedContacts[index], [field]: value }; | ||
setContacts(updatedContacts); | ||
}; | ||
|
||
// Custom announcement for adding rows | ||
const customAddAnnouncement = (rowNumber: number, rowGroupLabelPrefix: string) => `New ${rowGroupLabelPrefix.toLowerCase()} ${rowNumber} added.`; | ||
|
||
// Custom announcement for removing rows | ||
const customRemoveAnnouncement = (rowNumber: number, rowGroupLabelPrefix: string) => { | ||
const removedIndex = rowNumber - 1; | ||
const removedContact = contacts[removedIndex]; | ||
if (removedContact?.name) { | ||
return `Removed ${rowGroupLabelPrefix.toLowerCase()} ${removedContact.name}.`; | ||
} | ||
return `${rowGroupLabelPrefix} ${rowNumber} removed.`; | ||
}; | ||
|
||
// Custom aria-label for remove buttons | ||
const customRemoveAriaLabel = (rowNumber: number, rowGroupLabelPrefix: string) => { | ||
const contactIndex = rowNumber - 1; | ||
const contact = contacts[contactIndex]; | ||
if (contact?.name) { | ||
return `Remove ${rowGroupLabelPrefix.toLowerCase()} ${contact.name}`; | ||
} | ||
return `Remove ${rowGroupLabelPrefix.toLowerCase()} in row ${rowNumber}`; | ||
}; | ||
|
||
return ( | ||
<Form> | ||
<FieldBuilder | ||
label="" | ||
labelInfo="" | ||
isRequired | ||
firstColumnLabel="Name" | ||
secondColumnLabel="Email" | ||
rowCount={contacts.length} | ||
onAddRow={handleAddContact} | ||
onRemoveRow={handleRemoveContact} | ||
onAddRowAnnouncement={customAddAnnouncement} | ||
onRemoveRowAnnouncement={customRemoveAnnouncement} | ||
removeButtonAriaLabel={customRemoveAriaLabel} | ||
addButtonContent="Add contact" | ||
> | ||
{({ focusRef, firstColumnAriaLabel, secondColumnAriaLabel }, index) => [ | ||
<TextInput | ||
key="name" | ||
ref={focusRef} | ||
type="text" | ||
value={contacts[index]?.name || ''} | ||
placeholder="Enter full name" | ||
onChange={(_event, value) => handleContactChange(index, 'name', value)} | ||
aria-label={firstColumnAriaLabel} | ||
isRequired | ||
/>, | ||
<TextInput | ||
key="email" | ||
type="email" | ||
value={contacts[index]?.email || ''} | ||
placeholder="[email protected]" | ||
onChange={(_event, value) => handleContactChange(index, 'email', value)} | ||
aria-label={secondColumnAriaLabel} | ||
isRequired | ||
/> | ||
]} | ||
</FieldBuilder> | ||
</Form> | ||
); | ||
}; | ||
|
||
export default FieldBuilderExample; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,234 @@ | ||
import React, { useState } from 'react'; | ||
import { | ||
Form, | ||
Select, | ||
SelectOption, | ||
SelectList, | ||
MenuToggle, | ||
MenuToggleElement, | ||
} from '@patternfly/react-core'; | ||
import { FieldBuilder } from '@patternfly/react-component-groups/dist/dynamic/FieldBuilder'; | ||
|
||
interface TeamMember { | ||
department: string; | ||
role: string; | ||
} | ||
|
||
export const FieldBuilderSelectExample: React.FunctionComponent = () => { | ||
const [ teamMembers, setTeamMembers ] = useState<TeamMember[]>([ | ||
{ department: '', role: '' } | ||
]); | ||
|
||
// State for managing which select dropdowns are open | ||
const [ departmentOpenStates, setDepartmentOpenStates ] = useState<boolean[]>([ false ]); | ||
const [ roleOpenStates, setRoleOpenStates ] = useState<boolean[]>([ false ]); | ||
|
||
// Handle adding a new team member row | ||
const handleAddTeamMember = (event: React.MouseEvent) => { | ||
// eslint-disable-next-line no-console | ||
console.log('Add button clicked:', event.currentTarget); | ||
const newTeamMembers = [ ...teamMembers, { department: '', role: '' } ]; | ||
setTeamMembers(newTeamMembers); | ||
// Add new open states for the selects | ||
setDepartmentOpenStates([ ...departmentOpenStates, false ]); | ||
setRoleOpenStates([ ...roleOpenStates, false ]); | ||
}; | ||
|
||
// Handle removing a team member row | ||
const handleRemoveTeamMember = (event: React.MouseEvent, index: number) => { | ||
// eslint-disable-next-line no-console | ||
console.log('Remove button clicked:', event.currentTarget, 'for index:', index); | ||
const newTeamMembers = teamMembers.filter((_, i) => i !== index); | ||
setTeamMembers(newTeamMembers); | ||
// Remove corresponding open states | ||
setDepartmentOpenStates(departmentOpenStates.filter((_, i) => i !== index)); | ||
setRoleOpenStates(roleOpenStates.filter((_, i) => i !== index)); | ||
}; | ||
|
||
// Handle updating team member data | ||
const handleTeamMemberChange = (index: number, field: keyof TeamMember, value: string) => { | ||
const updatedTeamMembers = [ ...teamMembers ]; | ||
updatedTeamMembers[index] = { ...updatedTeamMembers[index], [field]: value }; | ||
setTeamMembers(updatedTeamMembers); | ||
}; | ||
|
||
// Handle department select open/close | ||
const handleDepartmentToggle = (index: number) => { | ||
const newOpenStates = [ ...departmentOpenStates ]; | ||
newOpenStates[index] = !newOpenStates[index]; | ||
setDepartmentOpenStates(newOpenStates); | ||
}; | ||
|
||
// Handle role select open/close | ||
const handleRoleToggle = (index: number) => { | ||
const newOpenStates = [ ...roleOpenStates ]; | ||
newOpenStates[index] = !newOpenStates[index]; | ||
setRoleOpenStates(newOpenStates); | ||
}; | ||
|
||
// Handle department selection | ||
const handleDepartmentSelect = (index: number, _event: React.MouseEvent<Element, MouseEvent> | undefined, value: string | number | undefined) => { | ||
handleTeamMemberChange(index, 'department', value as string); | ||
const newOpenStates = [ ...departmentOpenStates ]; | ||
newOpenStates[index] = false; | ||
setDepartmentOpenStates(newOpenStates); | ||
}; | ||
|
||
// Handle role selection | ||
const handleRoleSelect = (index: number, _event: React.MouseEvent<Element, MouseEvent> | undefined, value: string | number | undefined) => { | ||
handleTeamMemberChange(index, 'role', value as string); | ||
const newOpenStates = [ ...roleOpenStates ]; | ||
newOpenStates[index] = false; | ||
setRoleOpenStates(newOpenStates); | ||
}; | ||
|
||
// Custom announcement for adding rows | ||
const customAddAnnouncement = (rowNumber: number, rowGroupLabelPrefix: string) => `New ${rowGroupLabelPrefix.toLowerCase()} ${rowNumber} added.`; | ||
|
||
// Custom announcement for removing rows | ||
const customRemoveAnnouncement = (rowNumber: number, rowGroupLabelPrefix: string) => { | ||
const removedIndex = rowNumber - 1; | ||
const removedTeamMember = teamMembers[removedIndex]; | ||
if (removedTeamMember?.department && removedTeamMember?.role) { | ||
return `Removed ${rowGroupLabelPrefix.toLowerCase()} ${removedTeamMember.role} from ${removedTeamMember.department}.`; | ||
} | ||
return `${rowGroupLabelPrefix} ${rowNumber} removed.`; | ||
}; | ||
|
||
// Custom aria-label for remove buttons | ||
const customRemoveAriaLabel = (rowNumber: number, rowGroupLabelPrefix: string) => { | ||
const teamMemberIndex = rowNumber - 1; | ||
const teamMember = teamMembers[teamMemberIndex]; | ||
if (teamMember?.department && teamMember?.role) { | ||
return `Remove ${rowGroupLabelPrefix.toLowerCase()} ${teamMember.role} from ${teamMember.department}`; | ||
} | ||
return `Remove ${rowGroupLabelPrefix.toLowerCase()} in row ${rowNumber}`; | ||
}; | ||
|
||
const departmentOptions = [ | ||
{ label: 'Choose a department', value: '', disabled: true }, | ||
{ label: 'Engineering', value: 'engineering' }, | ||
{ label: 'Marketing', value: 'marketing' }, | ||
{ label: 'Sales', value: 'sales' }, | ||
{ label: 'Human Resources', value: 'hr' }, | ||
{ label: 'Finance', value: 'finance' } | ||
]; | ||
|
||
const roleOptions = [ | ||
{ label: 'Choose a role', value: '', disabled: true }, | ||
{ label: 'Manager', value: 'manager' }, | ||
{ label: 'Senior', value: 'senior' }, | ||
{ label: 'Junior', value: 'junior' }, | ||
{ label: 'Intern', value: 'intern' }, | ||
{ label: 'Contractor', value: 'contractor' } | ||
]; | ||
|
||
return ( | ||
<Form> | ||
<FieldBuilder | ||
label="" | ||
labelInfo="" | ||
|
||
isRequired | ||
firstColumnLabel="Department" | ||
secondColumnLabel="Role" | ||
rowCount={teamMembers.length} | ||
onAddRow={handleAddTeamMember} | ||
onRemoveRow={handleRemoveTeamMember} | ||
onAddRowAnnouncement={customAddAnnouncement} | ||
onRemoveRowAnnouncement={customRemoveAnnouncement} | ||
removeButtonAriaLabel={customRemoveAriaLabel} | ||
rowGroupLabelPrefix="Team member" | ||
addButtonContent="Add team member" | ||
> | ||
{({ focusRef, firstColumnAriaLabel, secondColumnAriaLabel }, index) => [ | ||
<Select | ||
key="department" | ||
id={`department-select-${index}`} | ||
isOpen={departmentOpenStates[index] || false} | ||
selected={teamMembers[index]?.department || ''} | ||
onSelect={(event, value) => handleDepartmentSelect(index, event, value)} | ||
onOpenChange={(isOpen) => { | ||
const newOpenStates = [ ...departmentOpenStates ]; | ||
newOpenStates[index] = isOpen; | ||
setDepartmentOpenStates(newOpenStates); | ||
}} | ||
toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( | ||
<MenuToggle | ||
ref={(element) => { | ||
// Handle both the toggle ref and focus ref | ||
if (typeof toggleRef === 'function') { | ||
toggleRef(element); | ||
} else if (toggleRef && 'current' in toggleRef && toggleRef.current !== element) { | ||
(toggleRef as React.MutableRefObject<MenuToggleElement | null>).current = element; | ||
} | ||
focusRef(element); | ||
}} | ||
onClick={() => handleDepartmentToggle(index)} | ||
isExpanded={departmentOpenStates[index] || false} | ||
aria-label={firstColumnAriaLabel} | ||
|
||
style={{ width: '100%' }} | ||
> | ||
{teamMembers[index]?.department ? | ||
departmentOptions.find(opt => opt.value === teamMembers[index]?.department)?.label || 'Choose a department' | ||
: 'Choose a department'} | ||
</MenuToggle> | ||
)} | ||
shouldFocusToggleOnSelect | ||
> | ||
<SelectList> | ||
{departmentOptions.map((option, optionIndex) => ( | ||
<SelectOption | ||
key={optionIndex} | ||
value={option.value} | ||
isDisabled={option.disabled} | ||
> | ||
{option.label} | ||
</SelectOption> | ||
))} | ||
</SelectList> | ||
</Select>, | ||
<Select | ||
key="role" | ||
id={`role-select-${index}`} | ||
isOpen={roleOpenStates[index] || false} | ||
selected={teamMembers[index]?.role || ''} | ||
onSelect={(event, value) => handleRoleSelect(index, event, value)} | ||
onOpenChange={(isOpen) => { | ||
const newOpenStates = [ ...roleOpenStates ]; | ||
newOpenStates[index] = isOpen; | ||
setRoleOpenStates(newOpenStates); | ||
}} | ||
toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( | ||
<MenuToggle | ||
ref={toggleRef} | ||
onClick={() => handleRoleToggle(index)} | ||
isExpanded={roleOpenStates[index] || false} | ||
aria-label={secondColumnAriaLabel} | ||
style={{ width: '100%' }} | ||
> | ||
{teamMembers[index]?.role ? | ||
roleOptions.find(opt => opt.value === teamMembers[index]?.role)?.label || 'Choose a role' | ||
: 'Choose a role'} | ||
</MenuToggle> | ||
)} | ||
shouldFocusToggleOnSelect | ||
> | ||
<SelectList> | ||
{roleOptions.map((option, optionIndex) => ( | ||
<SelectOption | ||
key={optionIndex} | ||
value={option.value} | ||
isDisabled={option.disabled} | ||
> | ||
{option.label} | ||
</SelectOption> | ||
))} | ||
</SelectList> | ||
</Select> | ||
]} | ||
</FieldBuilder> | ||
</Form> | ||
); | ||
}; | ||
|
||
export default FieldBuilderSelectExample; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these intended to be used for anything?