Skip to content

Commit

Permalink
fix(atomic): fix accessibility issues with pager/results per page but…
Browse files Browse the repository at this point in the history
…tons (#4761)

This PR fixes the accessibility issues with the pager/results per page
buttons. More specifically, it was not possible to navigating between
pager or results per page buttons without selecting one, triggering a
refresh of the results.

The solution is to have the buttons inside a toolbar and implement the
logic described here:

https://www.w3.org/WAI/ARIA/apg/patterns/radio/#:~:text=For%20Radio%20Group%20Contained%20in%20a%20Toolbar

https://coveord.atlassian.net/browse/KIT-3767
  • Loading branch information
fpbrault authored Dec 12, 2024
1 parent e25890d commit 4c50969
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const Choices: FunctionalComponent<ChoicesProps> = ({
class="btn-page focus-visible:bg-neutral-light"
part={parts.join(' ')}
text={text}
selectWhenFocused={false}
></RadioButton>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const PagerPageButton: FunctionalComponent<PagerPageButtonProps> = (
return (
<RadioButton
{...props}
selectWhenFocused={false}
key={props.page}
style="outline-neutral"
checked={props.isSelected}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const PagerNavigation: FunctionalComponent<PagerNavigationProps> = (
) => {
return (
<nav aria-label={props.i18n.t('pagination')}>
<div part="buttons" class="flex flex-wrap gap-2">
<div part="buttons" role="toolbar" class="flex flex-wrap gap-2">
{...children}
</div>
</nav>
Expand Down
57 changes: 57 additions & 0 deletions packages/atomic/src/components/common/radio-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {

export interface RadioButtonProps {
groupName: string;
selectWhenFocused?: boolean;
onChecked?(): void;
style?: ButtonStyle;
key?: string | number;
Expand Down Expand Up @@ -39,6 +40,61 @@ export const RadioButton: FunctionalComponent<RadioButtonProps> = (props) => {
classNames.push(props.class);
}

const handleKeyDown = (event: KeyboardEvent) => {
if (props.selectWhenFocused !== false) {
return;
}
const {key} = event;
const radioGroup = (event.currentTarget as HTMLElement).parentNode;

if (!radioGroup || !isArrowKey(key)) {
return;
}

event.preventDefault();

const buttons = getRadioButtons(radioGroup);
const currentIndex = getCurrentIndex(
buttons,
event.currentTarget as HTMLInputElement
);
const newIndex = getNewIndex(key, currentIndex, buttons.length);

if (buttons[newIndex]) {
buttons[newIndex].focus();
}
};

const isArrowKey = (key: string) => {
return ['ArrowLeft', 'ArrowRight', 'ArrowDown', 'ArrowUp'].includes(key);
};

const getRadioButtons = (radioGroup: ParentNode) => {
return Array.from(
radioGroup.querySelectorAll('[type="radio"]')
) as HTMLInputElement[];
};

const getCurrentIndex = (
buttons: HTMLInputElement[],
currentButton: HTMLInputElement
) => {
return buttons.findIndex((button) => button === currentButton);
};

const getNewIndex = (key: string, currentIndex: number, length: number) => {
switch (key) {
case 'ArrowLeft':
case 'ArrowUp':
return (currentIndex - 1 + length) % length;
case 'ArrowRight':
case 'ArrowDown':
return (currentIndex + 1) % length;
default:
return currentIndex;
}
};

const attributes = {
name: props.groupName,
key: props.key,
Expand All @@ -53,6 +109,7 @@ export const RadioButton: FunctionalComponent<RadioButtonProps> = (props) => {

return (
<input
onKeyDown={handleKeyDown}
type="radio"
onChange={(e) =>
(e.currentTarget as HTMLInputElement).checked && props.onChecked?.()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export class AtomicResultsPerPage implements InitializableComponent {
hasItems={this.searchStatusState.hasResults}
isAppLoaded={this.bindings.store.isAppLoaded()}
>
<div class="flex items-center">
<div class="flex items-center" role="toolbar" aria-label={this.label}>
<Label>{this.label}</Label>
<FieldsetGroup label={this.label}>
<Choices
Expand Down

0 comments on commit 4c50969

Please sign in to comment.