Skip to content
This repository has been archived by the owner on Oct 6, 2020. It is now read-only.

Commit

Permalink
feat(Accessibility): Makes checkbox/radio accessible. Focus States up…
Browse files Browse the repository at this point in the history
…dated. (#90)
  • Loading branch information
mathewmorris authored Dec 30, 2019
1 parent 5972b98 commit 1eecf79
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 37 deletions.
64 changes: 53 additions & 11 deletions src/Button/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,24 @@ const loadingCss = (height, color) => css`
const StyledButton = createComponent({
name: 'Button',
tag: 'button',
style: ({ hasText, leftIcon, rightIcon, variant, size, theme, block, disabled, loading, borderRadius }) => {
style: ({
hasText,
leftIcon,
rightIcon,
variant,
size,
theme,
block,
disabled,
loading,
borderRadius = theme.radius,
colorFocus = theme.colors.colorFocus,
}) => {
const variantStyles = getComponentVariant(theme, 'Button', variant);
const sizeStyles = getComponentSize(theme, 'Button', size);

return css`
position: relative;
display: inline-block;
cursor: pointer;
text-transform: capitalize;
Expand All @@ -56,13 +69,28 @@ const StyledButton = createComponent({
font-family: inherit;
font-weight: bold;
appearance: none;
border-radius: ${borderRadius || theme.radius}px;
border-radius: ${borderRadius}px;
pointer-events: ${disabled ? 'none' : 'auto'};
width: ${block ? '100%' : 'auto'};
border: 1px solid transparent;
transition: 175ms;
white-space: nowrap;
user-select: none;
outline: none;
&:before {
transition: opacity 250ms;
content: '';
position: absolute;
left: -5px;
top: -5px;
width: calc(100% + 2px);
height: calc(100% + 2px);
z-index: 0;
opacity: 0;
border: 4px solid ${colorFocus};
border-radius: ${borderRadius + 4}px;
}
${leftIcon &&
hasText &&
Expand All @@ -85,6 +113,12 @@ const StyledButton = createComponent({
opacity: 0.75;
}
&:focus {
&:before {
opacity: 1;
}
}
${loading && loadingCss(sizeStyles.height, variantStyles.color)};
${variantStyles}
${sizeStyles};
Expand All @@ -96,15 +130,23 @@ const StyledButton = createComponent({
const renderIcon = (icon, props) => <Icon name={icon} {...props} />;

/** Custom button styles for actions in forms, dialogs, and more with support for multiple sizes, states, and more. We include several predefined button styles, each serving its own semantic purpose, with a few extras thrown in for more control. */
const Button = React.forwardRef(({ children, leftIcon, leftIconProps, rightIcon, rightIconProps, ...rest }, ref) => (
<StyledButton ref={ref} hasText={!!children} leftIcon={leftIcon} rightIcon={rightIcon} {...rest}>
<Flex alignItems="center" justifyContent="center">
{leftIcon && renderIcon(leftIcon, leftIconProps)}
{children}
{rightIcon && renderIcon(rightIcon, rightIconProps)}
</Flex>
</StyledButton>
));
const Button = React.forwardRef(
({ children, leftIcon, leftIconProps, rightIcon, rightIconProps, colorFocus, ...rest }, ref) => (
<StyledButton
ref={ref}
hasText={!!children}
leftIcon={leftIcon}
rightIcon={rightIcon}
colorFocus={colorFocus}
{...rest}>
<Flex alignItems="center" justifyContent="center">
{leftIcon && renderIcon(leftIcon, leftIconProps)}
{children}
{rightIcon && renderIcon(rightIcon, rightIconProps)}
</Flex>
</StyledButton>
)
);

Button.propTypes = {
variant: PropTypes.string,
Expand Down
130 changes: 114 additions & 16 deletions src/Form/Checkbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,95 @@ const HiddenInput = createComponent({
name: 'CheckboxInput',
tag: 'input',
style: css`
display: none;
pointer-events: ${p => (p.disabled ? 'none' : 'auto')};
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
left: 1px;
white-space: nowrap;
`,
});

const CheckIcon = createComponent({
name: 'CheckIcon',
as: Icon,
style: ({ theme, color, iconSize }) => {
const checkIconStyles = getComponentSize(theme, 'CheckIcon', iconSize);

return css`
color: ${theme.colors[color]};
position: absolute;
z-index: 1;
${checkIconStyles}
`;
},
});

const CheckboxIcon = createComponent({
name: 'CheckboxIcon',
as: Icon,
style: ({ theme, iconSize }) => {
const sizeStyles = getComponentSize(theme, 'CheckboxIcon', iconSize);
as: 'div',
style: ({ theme, size, color, isChecked, isFocused, isRadio, colorFocus = theme.colors.colorFocus }) => {
const sizeStyles = getComponentSize(theme, 'CheckboxIcon', size);
const radioStyles = getComponentSize(theme, 'RadioIcon', size);

return css`
font-size: 24px;
transition: color 125ms;
transition: 250ms;
position: relative;
border: solid ${theme.colors[color]};
&:before, &:after {
transition: opacity 250ms;
content: '';
position: absolute;
opacity: 0;
}
${sizeStyles}
${isChecked &&
css`
background: ${theme.colors[color]};
border-color: ${theme.colors[color]};
${isRadio &&
css`
background: white;
&:after {
opacity: 1;
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: ${theme.colors[color]};
${radioStyles}
}
`}
`}
${isFocused &&
css`
&:before {
opacity: 1;
border: 4px solid ${colorFocus};
z-index: -1;
}
`}
${isRadio &&
css`
border-radius: 50%;
&:before {
border-radius: 50%;
}
`}
`;
},
});
Expand Down Expand Up @@ -90,19 +163,19 @@ export class Checkbox extends React.Component {
valueTrue: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.bool]),
valueFalse: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.bool]),
onChange: PropTypes.func,
iconOn: PropTypes.string,
iconOff: PropTypes.string,
size: PropTypes.string,
horizontal: PropTypes.bool,
disabled: PropTypes.bool,
styles: PropTypes.shape(),
colorOn: PropTypes.string,
colorOff: PropTypes.string,
ariaLabel: PropTypes.string,
checkIconColor: PropTypes.string,
checkIcon: PropTypes.string,
colorFocus: PropTypes.string,
};

static defaultProps = {
iconOn: 'checkbox-marked',
iconOff: 'checkbox-blank-outline',
size: 'md',
valueTrue: true,
valueFalse: false,
Expand All @@ -112,6 +185,9 @@ export class Checkbox extends React.Component {
onChange() {},
disabled: false,
styles: {},
label: 'Checkbox',
checkIconColor: 'white',
checkIcon: 'check',
};

static getDerivedStateFromProps(props, state) {
Expand Down Expand Up @@ -147,41 +223,63 @@ export class Checkbox extends React.Component {
);
};

handleFocus = () => {
this.setState({ isFocused: !this.state.isFocused });
};

render() {
const {
label,
id,
error,
name,
size,
iconOn,
iconOff,
checkIcon,
checkIconColor,
isRadio,
colorOn,
colorOff,
horizontal,
disabled,
styles,
ariaLabel,
colorFocus,
} = this.props;
const { checked } = this;
const { isFocused } = this.state;

return (
<>
<CheckboxContainer
horizontal={horizontal}
style={styles.CheckboxContainer}
checked={checked}
disabled={disabled}>
disabled={disabled}
htmlFor={id}>
<HiddenInput
aria-label={ariaLabel || label}
id={id}
name={name}
type="checkbox"
checked={checked}
disabled={disabled}
onChange={this.handleChange}
onFocus={this.handleFocus}
onBlur={this.handleFocus}
/>

<Flex>
<CheckboxIcon iconSize={size} color={checked ? colorOn : colorOff} name={checked ? iconOn : iconOff} />
{checked && !isRadio && (
<CheckIcon name={checkIcon} color={checkIconColor} iconSize={size} isRadio={isRadio} />
)}
<CheckboxIcon
size={size}
color={checked ? colorOn : colorOff}
isChecked={checked}
isFocused={isFocused}
isRadio={isRadio}
colorFocus={colorFocus}
/>

{label && (
<CheckboxLabel size={size} style={styles.Label}>
Expand All @@ -191,7 +289,7 @@ export class Checkbox extends React.Component {
</Flex>
</CheckboxContainer>

{!this.state.focused && error ? <FormError>{error}</FormError> : null}
{!isFocused && error ? <FormError>{error}</FormError> : null}
</>
);
}
Expand Down
15 changes: 14 additions & 1 deletion src/Form/Checkbox.stories.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { boolean } from '@storybook/addon-knobs';
import { boolean, text } from '@storybook/addon-knobs';
import Box from '../Box';
import { Checkbox } from './Checkbox';

Expand Down Expand Up @@ -27,6 +27,19 @@ export const Colors = () => (
<Checkbox id="checkbox" name="checkbox" colorOn="green" colorOff="red" label="Red Off, Green On" />
);

export const FocusColor = () => (
<>
<Checkbox id="checkbox" name="checkbox" label="Default Focus Color" />
<Checkbox id="checkbox" name="checkbox" label="Default Focus Color" />
<Checkbox
id="checkbox"
name="checkbox"
label="Custom Focus Color"
colorFocus={text('Custom Focus Color', 'papayawhip')}
/>
</>
);

export const Sizes = () => (
<>
<Checkbox id="checkbox" name="checkbox" label="I'm a checkbox" size="sm" />
Expand Down
4 changes: 3 additions & 1 deletion src/Form/RadioGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class RadioGroup extends Component {
};

render() {
const { choices, error, horizontal, label, name, ...checkboxProps } = this.props;
const { choices, error, horizontal, label, name, colorFocus, ...checkboxProps } = this.props;

return (
<StyledRadioGroup>
Expand All @@ -90,6 +90,8 @@ export class RadioGroup extends Component {
valueTrue={value}
valueFalse={value}
onChange={this.handleChange}
colorFocus={colorFocus}
isRadio
/>
);
})}
Expand Down
2 changes: 1 addition & 1 deletion src/Form/RadioGroup.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const Vertical = () => (
export const Horizontal = () => <RadioGroup horizontal name="radio" id="radio" choices={defaultValues} />;

export const Colors = () => (
<RadioGroup colorOn="green" colorOff="red" name="radio" id="radio" choices={defaultValues} value="radio2" />
<RadioGroup colorOn="green" colorOff="red" name="radio" id="radio" choices={defaultValues} />
);

export const Sizes = () => (
Expand Down
Loading

0 comments on commit 1eecf79

Please sign in to comment.