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

fix: radio accessiblity #25

Merged
merged 6 commits into from
Jun 24, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 84 additions & 26 deletions src/components/Radio.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,75 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import styled, { css } from 'styled-components';
import { rgba } from 'polished';
import { color, typography } from './shared/styles';

const RadioWrapper = styled.div`
display: flex;
align-items: center;
flex-wrap: wrap;
`;

const Label = styled.label`
cursor: pointer;
font-size: ${typography.size.s2}px;
font-weight: ${typography.weight.bold};
min-height: 1em;
position: relative;
display: block;
height: 1em;
display: flex;
align-items: center;
`;

const OptionalText = styled.span`
${props =>
props.hideLabel &&
css`
border: 0px !important;
clip: rect(0 0 0 0) !important;
-webkit-clip-path: inset(100%) !important;
clip-path: inset(100%) !important;
height: 1px !important;
overflow: hidden !important;
padding: 0px !important;
position: absolute !important;
white-space: nowrap !important;
width: 1px !important;
Copy link
Contributor

Choose a reason for hiding this comment

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

Were these !important tags necessary to make the styles apply correctly? Or are they more for being explicit about the behavior? Does look to me like they should work without !important, but maybe I am missing something.

Copy link
Member

Choose a reason for hiding this comment

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

This is extra safety to make sure no inherited styles collide. 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

@kylesuss Just for extra info, this part has !important which is usually veeeeery bad, but this is a standard trick to make it rendered out of the viewport, but still visible by screen readers.
Every css framework has a class with this type of css, for example bootstrap has sr-only.

Copy link
Contributor

Choose a reason for hiding this comment

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

Nice. Thanks for the explanation @jsomsanith . Makes sense!

`}
`;

const Error = styled.span`
font-weight: ${typography.weight.regular};
font-size: ${typography.size.s2}px;
color: ${color.negative};
margin-left: 6px;
height: 1em;
display: flex;
align-items: center;
`;

const LabelText = styled.div``;
const SublabelText = styled.div`
const LabelText = styled.span``;

const Description = styled.div`
font-size: ${typography.size.s1}px;
font-weight: ${typography.weight.regular};
margin-top: 4px;
color: ${color.mediumdark};
margin-top: 4px;
width: 100%;
`;

const Input = styled.input.attrs({ type: 'radio' })`
float: left;
margin: 0 0.6em 0 0;
visibility: hidden;
opacity: 0;

& + ${LabelText} {
display: block;
line-height: 1;
overflow: hidden;

&:before,
&:after {
transition: all 150ms ease-out;
position: absolute;
top: 0;
left: 0;
height: 14px;
width: 14px;
height: 1em;
width: 1em;
content: '';
display: block;
border-radius: 3em;
Expand All @@ -54,10 +80,18 @@ const Input = styled.input.attrs({ type: 'radio' })`
box-shadow: ${color.mediumdark} 0 0 0 1px inset;
}

&:focus + ${LabelText}:before {
box-shadow: ${color.primary} 0 0 0 1px inset;
}

&:checked + ${LabelText}:before {
box-shadow: ${color.primary} 0 0 0 1px inset;
}

&:checked:focus + ${LabelText}:before {
box-shadow: ${color.primary} 0 0 0 1px inset, ${rgba(color.primary, 0.3)} 0 0 5px 2px;
}

& + ${LabelText}:after {
transform: scale3d(0, 0, 1);

Expand All @@ -76,32 +110,56 @@ const Input = styled.input.attrs({ type: 'radio' })`
}
`;

export function Radio({ value, label, sublabel, error, className, ...props }) {
export function Radio({ id, label, description, error, hideLabel, value, className, ...props }) {
let errorId;
let descriptionId;
let ariaDescribedBy;

if (error) {
errorId = `${id}-error`;
ariaDescribedBy = errorId;
}
if (description) {
descriptionId = `${id}-description`;
ariaDescribedBy = `${ariaDescribedBy} ${descriptionId}`;
}

return (
<Label className={className}>
<Input value={value} {...props} type="radio" />

<LabelText>
{label}
{error && <Error>{error}</Error>}
{sublabel && <SublabelText>{sublabel}</SublabelText>}
</LabelText>
</Label>
<RadioWrapper>
<Label className={className}>
<Input
{...props}
id={id}
aria-describedby={ariaDescribedBy}
aria-invalid={!!error}
type="radio"
value={value}
/>
<LabelText>
<OptionalText hideLabel={hideLabel}>{label}</OptionalText>
</LabelText>
</Label>
{error && <Error id={errorId}>{error}</Error>}
{description && <Description id={descriptionId}>{description}</Description>}
</RadioWrapper>
);
}

Radio.propTypes = {
id: PropTypes.string.isRequired,
value: PropTypes.string,
label: PropTypes.string,
sublabel: PropTypes.string,
hideLabel: PropTypes.bool,
description: PropTypes.string,
error: PropTypes.string,
className: PropTypes.string,
};

Radio.defaultProps = {
value: '',
label: null,
sublabel: null,
hideLabel: false,
description: null,
error: null,
className: null,
};
16 changes: 10 additions & 6 deletions src/components/Radio.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ storiesOf('Design System|forms/Radio', module)
.addParameters({ component: Radio })
.add('all radios', () => (
<form>
<Radio label="Mice" value="mice" checked onChange={onChange} />
<Radio label="Dogs" value="dogs" onChange={onChange} />
<Radio label="Cats" onChange={onChange} error="There's a snake in my boots" />
<Radio label="Dogs" sublabel="15 canines" value="dogs" onChange={onChange} />
<Radio id="Mice" label="Mice" value="mice" checked onChange={onChange} />
<Radio id="Dogs" label="Dogs" value="dogs" onChange={onChange} />
<Radio id="Cats" label="Cats" onChange={onChange} error="There's a snake in my boots" />
<Radio id="Dogs" label="Dogs" description="15 canines" value="dogs" onChange={onChange} />
</form>
))
.add('unchecked', () => <Radio value="mice" onChange={onChange} />)
.add('checked', () => <Radio value="dogs" checked onChange={onChange} />);
.add('unchecked', () => (
<Radio id="Mice" label="Mice" hideLabel value="mice" onChange={onChange} />
))
.add('checked', () => (
<Radio id="Dogs" label="Dogs" hideLabel value="dogs" checked onChange={onChange} />
));