Skip to content

Instantly share code, notes, and snippets.

@chrispcode
Created November 24, 2019 12:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chrispcode/f623120e34e389de04ff25d9d9c90ea7 to your computer and use it in GitHub Desktop.
Save chrispcode/f623120e34e389de04ff25d9d9c90ea7 to your computer and use it in GitHub Desktop.
Let's Make a React Radio That Will Pass A11Y - Part 2 State
import React, { useState, MouseEvent, KeyboardEvent } from 'react';
import {
Circle,
Container,
Option,
Wrap
} from './styled';
const keyCodes = {
arrowLeft: 37,
arrowUp: 38,
arrowRight: 39,
arrowDown: 40,
space: 32
};
interface Option {
key: string
value: string
id?: string
}
interface Props {
id: string
label: string
options: Option[]
}
export function Radio({
id,
label,
options
}: Props) {
const [focusId, setFocusId] = useState('');
const [selectedId, setSelectedId] = useState('');
const optionsWithIds = options.map((option) => {
const clone = { ...option };
clone.id = clone.id || `radio-option-${id}-${option.value}`;
return clone;
});
function handleInitialContainerFocus() {
if (!focusId) {
setFocusId(optionsWithIds[0].id);
}
}
function handleOptionClick(option: Option) {
return (event: MouseEvent) => {
setFocusId(option.id);
setSelectedId(option.id);
};
}
function handleContainerKeyPress(
event: KeyboardEvent
) {
switch (event.keyCode) {
case keyCodes.arrowLeft:
case keyCodes.arrowUp: {
event.preventDefault();
const previousOptionIndex = optionsWithIds.findIndex(
(option) => option.id === focusId
) - 1;
if (previousOptionIndex >= 0) {
const previousOption = optionsWithIds[previousOptionIndex].id;
setSelectedId(previousOption);
setFocusId(previousOption);
}
break;
}
case keyCodes.arrowDown:
case keyCodes.arrowRight: {
event.preventDefault();
const nextOptionIndex = optionsWithIds.findIndex(
(option) => option.id === focusId
) + 1;
if (nextOptionIndex < optionsWithIds.length) {
const nextOption = optionsWithIds[nextOptionIndex].id;
setSelectedId(nextOption);
setFocusId(nextOption);
}
break;
}
case keyCodes.space: {
event.preventDefault();
setSelectedId(focusId);
break;
}
default: break;
}
}
function renderOptions() {
return optionsWithIds.map((option) => (
<Option
key={option.id}
id={option.id}
role="radio"
aria-checked={selectedId === option.id}
onClick={handleOptionClick(option)}
>
<Circle />
{option.key}
</Option>
));
}
const labelId = `radio-label-${id}`;
return (
<Wrap>
<h3 id={labelId}>
{label}
</h3>
<Container
tabIndex={0}
role="radiogroup"
aria-labelledby={labelId}
aria-activedescendant={focusId}
onFocus={handleInitialContainerFocus}
onKeyDown={handleContainerKeyPress}
>
{renderOptions()}
</Container>
</Wrap>
);
}
export default Radio;
import styled, { css } from 'styled-components';
export const Wrap = styled.div`
font-family: "Open Sans";
& > h3 {
margin: 0 0 24px 0;
}
`;
export const Circle = styled.div`
width: 24px;
height: 24px;
margin-right: 8px;
border: 2px solid #CED6E0;
border-radius: 50%;
box-sizing: border-box;
`;
export const Option = styled.li`
display: flex;
cursor: pointer;
${(props) => props['aria-checked'] && css`
& > ${Circle} {
border: 6px solid #3B3B98;
}
`}
`;
export const Container = styled.ul`
padding:0;
margin: 0;
list-style: none;
outline: none;
& > ${Option}:nth-child(n+2) {
margin-top: 8px;
}
&:focus {
#${(props) => props['aria-activedescendant']} > ${Circle} {
box-shadow: 0 0 0 8px #F1F2F6;
}
}
`;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment