Skip to content
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

feat(SegmentedControl): a11y - set role to radiogroup #6691

Merged

Conversation

bvandercar-vt
Copy link
Contributor

@bvandercar-vt bvandercar-vt commented Feb 1, 2024

Improves a11y of new SegmentedControl item, giving it the radiogroup role and also allowing it to be controlled with arrow keys.

  • Added Tests

@adidahiya
Copy link
Contributor

Various open source UI toolkits have different approaches for Segmented Control a11y:

Can you explain the motivations for your change and make an argument for setting the default as you've done in this PR?

I think a safer, better approach might be to add good documentation to the component docs suggesting that users should set their own ARIA role appropriately by specifying the role attribute.

@bvandercar-vt
Copy link
Contributor Author

bvandercar-vt commented Mar 14, 2024

@adidahiya that is tricky. After reading through those, I agree my original choice of listbox was incorrect.

It sounds like radiogroup is the best option, I changed the code to that. Despite what that Primer link says, I see no evidence why it shouldn't be radiogroup when that's exactly how it behaves.

If you would like me to add a role prop-- role?="group" | "list" | "radiogroup"-- and then we set the children roles and aria props based on that as well, I'm up for it. But IMO, I don't think anyone would complain about it being hard set to radiogroup

@bvandercar-vt bvandercar-vt changed the title feat(SegmentedControl): make listbox role to fix a11y feat(SegmentedControl): a11y - set role to radiogroup Jun 24, 2024
Copy link
Contributor

@evansjohnson evansjohnson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why it shouldn't be radiogroup when that's exactly how it behaves

Right now the individual buttons are focusable, not the whole element with focus moved between with arrow keys as in a radio group. The non-checked buttons should have tabIndex={-1} and we should hook up arrow key listeners to move between the options if we want this to behave like a radio group.

I too do not see the argument against treating this like a radio group for the reason of "which would require a save button to apply the changes" - I see no reference to this in this w3 example or the mdn docs.


The SegmentedControl docs do say "similar to a Radio group" so this seems worth doing to me, as long as we hook up the expected interaction behavior as well.

@evansjohnson evansjohnson self-assigned this Jun 26, 2024
@bvandercar-vt
Copy link
Contributor Author

@evansjohnson Done, added arrow key control and the proper tabIndex setting! Tested, works great.

Copy link
Contributor

@evansjohnson evansjohnson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is looking good - some last comments and then will leave to @ggdouglas for approval since he'll be owning the next BP release

* If false, down arrow returns same as left arrow, up arrow returns same as right arrow.
* @returns -1 for left, 1 for right, undefined if not an arrow keypress.
*/
export function getArrowKeyDirection(event: React.KeyboardEvent<HTMLElement>, upMovesLeft: boolean = true) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

upMovesLeft is interesting - are we currently following expected behavior correctly? it definitely makes sense in the slider case for up to move right but I'm wondering if up should always be to the right

this feels like something that may change based on locale, possibly in addition to component usage

not sure we need to figure this out now since this keeps existing behavior but leaving this comment for posterity at least

Copy link
Contributor Author

@bvandercar-vt bvandercar-vt Sep 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Slider: Definitely makes sense for up = right.

  • Tabs: the official aria tabs example doesn't have any effect from the up/down arrow keys, only left/right. So if anything, we should remove the effect of an up/down keypress at some point. But with this PR, the existing implementation goes unchanged.

  • SegmentedControl: since we are giving this the radiogroup role, here in the official example of this role you can see that up = left. The action description is given as well:
    image

@@ -92,34 +94,72 @@ export const SegmentedControl: React.FC<SegmentedControlProps> = React.forwardRe
value: controlledValue,
...htmlProps
} = props;
const [selectedIndex, setSelectedIndex] = React.useState<number>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should keep this code more like it was before - where we use this local state index only if using the defaultValue option to avoid duplicating state, then on every render reconcile the index of the user provided controlledValue with this local index as a fallback

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm it's tricky because we want to add keypress control with this PR, which is much easier to change to the next index vs. the next value. I can take a shot at it though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Reduced the diff a ton, mostly the same code as was before, but arrow keys work as indended.

@bvandercar-vt
Copy link
Contributor Author

@ggdouglas this should be ready for final review!

if (!focusedElement) return;

// must rely on DOM state because we have no way of mapping `focusedElement` to a React.JSX.Element
const enabledOptionElements = Array.from(outerElement.querySelectorAll("button")).filter(
Copy link
Contributor

@ggdouglas ggdouglas Sep 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we leverage the selector query to filter out disabled buttons? e.g.

Suggested change
const enabledOptionElements = Array.from(outerElement.querySelectorAll("button")).filter(
const enabledOptionElements = Array.from(outerElement.querySelectorAll("button:not(:disabled)"));

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, done!

Copy link
Contributor

@ggdouglas ggdouglas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this looks good! Will approve after linter errors are fixed.

@bvandercar-vt
Copy link
Contributor Author

@ggdouglas made some additional changes actually-- added a role prop so consumers can use a different aria role for the segmented control if they wish, since there seems to be some general debate on what the proper role is.

@bvandercar-vt
Copy link
Contributor Author

@ggdouglas the lint error is actually unrelated to this PR, it's happening in my other PRs as well. Here's the fix: #6972

@ggdouglas ggdouglas merged commit c5e8bdc into palantir:develop Sep 13, 2024
11 of 15 checks passed
@bvandercar-vt bvandercar-vt deleted the bvandercar/SegmentedControl-a11y branch November 7, 2024 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants