-
Notifications
You must be signed in to change notification settings - Fork 29
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
base: main
Are you sure you want to change the base?
Conversation
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.
Some initial comments below, let me know if you have any questions/concerns
* This allows each rendered row to have access to its own specific index. | ||
*/ | ||
export interface FieldRowHelpers { | ||
rowIndex: number; |
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.
Would we expect to be able to pass in rowIndex for any reason?
* Unique ID for this row group - use this for aria-labelledby associations | ||
*/ | ||
rowGroupId: string; |
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.
This really applies to all the props that are creating or being used in some way for the aria-labelledby on the inputs and such, but an alternative would be to use aria-label instead.
The pro to that is it's less content that needs to be rendered in the DOM mainly, possibly another pro is AT won't traverse to the visually hidden text (which whether it's a pro depends on whether it'd be something users wwould find helpful, so we don't really know at this time)
Either way probably makes sense, since we're dealing with a mixture of visible text and non-visible text to reference.
* Complete aria-labelledby string for the first column that includes both row and column context | ||
*/ | ||
firstColumnAriaLabelledBy: string; | ||
/** | ||
* Complete aria-labelledby string for the second column that includes both row and column context | ||
*/ |
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.
If we do want to keep the aria-labelledby approach, we should just update these descriptions so that it's clear that a space separated list of IDs referencing other elements must be passed in here, and that doing so allows custom labeling/overriding the default aria-labelledby
/** A callback triggered when the "Add" button is clicked. */ | ||
onAddRow: () => void; |
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.
Can we include the event object as a param here?
/** A callback triggered when the "Add" button is clicked. */ | ||
onAddRow: () => void; | ||
/** A callback triggered when a "Remove" button is clicked, which receives the index of the row to remove. */ | ||
onRemoveRow: (index: number) => void; |
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.
Same as above here, with event being the first param
// Enhanced onAddRow with focus management and announcements | ||
const handleAddRow = useCallback(() => { | ||
onAddRow(); | ||
announceChange(`New ${rowGroupLabelPrefix.toLowerCase()} added. ${rowGroupLabelPrefix} ${rowCount + 1}.`); |
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.
We should allow some way to customize what can be passed into this announceChange callback for both adding and removing a row. On adding a row a consumer may only want to say that a new row was added, rather than what the row number is. On removing a row, it may make sense to reference the "primary" input value (e.g. "Removed row John Smith").
}, [ onAddRow, announceChange, rowGroupLabelPrefix, rowCount ]); | ||
|
||
// Enhanced onRemoveRow with focus management and announcements | ||
const handleRemoveRow = useCallback((index: number) => { |
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.
For the logic here, I'm thinking that we shouldn't move focus to another destructive action, especially since there's no confirmation when removing a row. Rather than moving to another row's "remove" button, can we move focus to the first input in that row perhaps?
Though another thought I suppose is whether we should be providing this logic or if it should be up to the consumer. We don't handle any logic for adding a row - that's done in the examples. Maybe input from others on whether this sort of logic should be handled by component-groups.
|
||
// Enhanced onAddRow with focus management and announcements | ||
const handleAddRow = useCallback(() => { | ||
onAddRow(); |
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.
Depending on the resolution of the above comment, either in the component code or example code, when a new row is added we should shift focus to that new rows first input.
icon={<PlusCircleIcon />} | ||
{...addButtonProps} | ||
> | ||
{addButtonProps.children || 'Add another'} |
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.
Nit: we could add a "addButtonContent" prop instead of having to pass addButtonProps.children
inside an object.
<Button | ||
ref={createRemoveButtonRef(index)} | ||
variant="plain" | ||
aria-label={`Remove ${rowGroupLabelPrefix.toLowerCase()} ${rowNumber}`} |
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.
We should also allow customizing this aria-label further with a prop.
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.
Another batch of comments below.
Additionally, I've been thinking about it more, and using a Table for all of this might make a bit more sense to help link relationships between things.
Right now, traversing with VO, the "name" and "email" texts at the top of the columns are pretty standalone - there's really nothing stating what they're for or any relationship. So it can kinda give off a "name and email for what?" vibes. It's not really until you navigate to the inputs (assuming there are any) that it may become clearer what those texts mean/are for. Visually it works, but from an AT standpoint it may be less clear.
Using a Table would help link those relationships more clearly and immediately, since those would be proper column headers for something. You'll know "okay this is a Name column for some stuff".
Would you be willing to try building this out using a Table instead of just Grid layouts?
const addButton = document.querySelector('button[aria-label*="Add"]') as HTMLButtonElement; | ||
if (addButton) { | ||
addButton.focus(); | ||
} |
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.
This isn't being ran since there's no button with an aria-label of "Add". If we wanted to keep the querySelector we could add a unique ID to the add button and target that.
setContacts(newContacts); | ||
|
||
// Focus management: avoid focusing on destructive actions | ||
setTimeout(() => { |
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.
Two points for all the focus logic in these examples:
-
Spoke with Nicole this morning, and it'd be great if we can bake this focus logic inside the FieldBuilder rather than consumers needing to pass all this logic in. Maybe default logic that handles the typical use-case, with these onRemoveRow and onAddRow props potentially being a means for consumers to pass in their own custom logic if they need to.
-
This is really more of a nit and we can do it in a followup to investigate a bit more, but if possible it'd be great to not rely on a timeout for the focus logic. Would probably involve using a useEffect and having to track the previous length of newContacts to compare to the current length (tell if something was added or removed), etc. I'm personally always a bit wary of using timeouts for this sort of thing (even though I will use them if necessary), just because there may not be a guarantee the focus will occur after the specified timeout length, plus other side effects (for example, when I remove a row on the PR preview, the focus goes to the main page area briefly before moving to the correct row's input - so I VoiceOver starts announcing "PatternFly field builder, content", but gets cut off once focus does go to the input).
Not a blocker right now, so if you would prefer opening a followup for this I'd be good with that.
onAddRowAnnouncement={customAddAnnouncement} | ||
onRemoveRowAnnouncement={customRemoveAnnouncement} | ||
removeButtonAriaLabel={customRemoveAriaLabel} | ||
addButtonContent="Add team member" |
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.
Let's give these examples more unique add button texts.
* Ref callback to attach to the first focusable element in the row. | ||
* This enables automatic focus management when rows are added/removed. | ||
*/ | ||
focusRef: (element: HTMLElement | null) => void; |
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.
Does this ref actually get used for anything currently? The examples are using querySelectors and such to handle focusing logic so just curious.
aria-atomic="true" | ||
role="status" |
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.
We can remove these 2 attributes.
aria-atomic
isn't really relevant here since there's only really going to be a text string rendered inside this div. aria-atomic would be more helpful for something like a div that contained "7 messages", with "7" and "messages" being in their own <span>
elements. By default a live region on the div would only announce when the number changes which most likely isn't super helpful - adding aria atomic would help ensure that what gets announced is "8 messages" rather han just "8" for example.
the role of status is basically the same as setting aria-live of polite and aria-atomic of true. Since we don't need aria-atomic here, we can just remove the role as well.
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.
Hey! Just a few things:
- Can you please adjust the spacing/padding between the input fields to be 8px padding (pf-t--global--spacer--sm)?
- Can you please adjust the padding between the input fields and the input field labels to be 8px padding (pf-t--global--spacer--sm)?
- Can you change the inline link button to be a link button(no underline)? dont worry about the left alignment
- Can you please adjust the padding between the input fields and the link button to be 8px (pf-t--global--spacer--sm)?
2daea20
to
7deaff0
Compare
Field builder component. Closes #731