Skip to content

Commit

Permalink
feat(radio): Add theming support (#457)
Browse files Browse the repository at this point in the history
* feat(radio): Add theming to Radio and RadioGroup

* refactor(radio): Refactor styles to ensure the correct states

* feat(common): Abstract error ring color logic

* refactor: Use new getErrorColors function in checkbox and radiogroup

* fix(common): Add missing check to errorRing when no error is present
  • Loading branch information
anicholls committed Feb 20, 2020
1 parent 1162398 commit f268170
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 165 deletions.
33 changes: 16 additions & 17 deletions modules/checkbox/react/lib/Checkbox.tsx
@@ -1,6 +1,11 @@
import * as React from 'react';
import {styled, Themeable} from '@workday/canvas-kit-labs-react-core';
import {ErrorType, themedFocusRing, mouseFocusBehavior} from '@workday/canvas-kit-react-common';
import {
ErrorType,
themedFocusRing,
mouseFocusBehavior,
getErrorColors,
} from '@workday/canvas-kit-react-common';
import canvas, {
borderRadius,
colors,
Expand Down Expand Up @@ -173,34 +178,28 @@ const CheckboxInput = styled('input')<CheckboxProps>(
},
}),
({theme, error}) => {
let errorRingColor;
let errorRingBorderColor = 'transparent';
const errorColors = getErrorColors(error, theme);

if (error === ErrorType.Error) {
errorRingColor = theme.palette.error.main;
} else if (error === ErrorType.Alert) {
errorRingColor = theme.palette.alert.main;
errorRingBorderColor = theme.palette.alert.darkest;
} else {
return;
if (errorColors.outer === errorColors.inner) {
errorColors.outer = 'transparent';
}

const errorStyles = {
'& ~ div:first-of-type': {
border: `1px solid ${errorRingColor}`,
boxShadow: `0 0 0 1px ${errorRingColor}, 0 0 0 2px ${errorRingBorderColor}`,
border: `1px solid ${errorColors.inner}`,
boxShadow: `0 0 0 1px ${errorColors.inner}, 0 0 0 2px ${errorColors.outer}`,
},
'&:not(:checked):not(:disabled):not(:focus):hover, &:not(:checked):not(:disabled):active': {
'~ div:first-of-type': {
borderColor: errorRingColor,
borderColor: errorColors.inner,
},
},
'&:checked ~ div:first-of-type': {
borderColor: theme.palette.primary.main,
boxShadow: `
0 0 0 2px ${colors.frenchVanilla100},
0 0 0 4px ${errorRingColor},
0 0 0 5px ${errorRingBorderColor}`,
0 0 0 4px ${errorColors.inner},
0 0 0 5px ${errorColors.outer}`,
},
};
return {
Expand All @@ -209,8 +208,8 @@ const CheckboxInput = styled('input')<CheckboxProps>(
...mouseFocusBehavior({
...errorStyles,
'&:not(:checked):focus ~ div:first-of-type': {
border: `1px solid ${errorRingColor}`,
boxShadow: `0 0 0 1px ${errorRingColor}, 0 0 0 2px ${errorRingBorderColor}`,
border: `1px solid ${errorColors.inner}`,
boxShadow: `0 0 0 1px ${errorColors.inner}, 0 0 0 2px ${errorColors.outer}`,
},
}),
};
Expand Down
2 changes: 1 addition & 1 deletion modules/common/react/index.ts
@@ -1,5 +1,5 @@
export {default as focusRing, themedFocusRing} from './lib/styles/focusRing';
export {default as errorRing} from './lib/styles/errorRing';
export {default as errorRing, getErrorColors} from './lib/styles/errorRing';
export {default as accessibleHide} from './lib/styles/accessibleHide';
export {default as hideMouseFocus, mouseFocusBehavior} from './lib/styles/hideMouseFocus';
export {makeMq} from './lib/utils/makeMq';
Expand Down
63 changes: 41 additions & 22 deletions modules/common/react/lib/styles/errorRing.ts
Expand Up @@ -4,41 +4,60 @@ import {CSSObject} from '@emotion/core';
import {colors, inputColors} from '@workday/canvas-kit-react-core';
import chroma from 'chroma-js';

export default function errorRing(error?: ErrorType, theme?: CanvasTheme): CSSObject {
let errorBorderColor;
let errorBoxShadow;
const isAccessible = (foreground: string, background: string = colors.frenchVanilla100) => {
return chroma.contrast(foreground, background) >= 3;
};

export function getErrorColors(error?: ErrorType, theme?: CanvasTheme) {
if (error === ErrorType.Error) {
errorBorderColor = theme
? chroma.contrast(theme.palette.error.main, colors.frenchVanilla100) >= 3
? theme.palette.error.main
: theme.palette.error.darkest
: inputColors.error.border;
errorBoxShadow = `inset 0 0 0 1px ${
theme ? theme.palette.error.main : inputColors.error.border
}`;
if (theme) {
const palette = theme.palette.error;
return {
outer: isAccessible(palette.main) ? palette.main : palette.darkest,
inner: palette.main,
};
} else {
return {
outer: inputColors.error.border,
inner: inputColors.error.border,
};
}
} else if (error === ErrorType.Alert) {
errorBorderColor = theme
? chroma.contrast(theme.palette.alert.main, colors.frenchVanilla100) >= 3
? theme.palette.alert.main
: theme.palette.alert.darkest
: colors.cantaloupe600;
errorBoxShadow = `inset 0 0 0 2px ${
theme ? theme.palette.alert.main : inputColors.warning.border
}`;
if (theme) {
const palette = theme.palette.alert;
return {
outer: isAccessible(palette.main) ? palette.main : palette.darkest,
inner: palette.main,
};
} else {
return {
outer: colors.cantaloupe600,
inner: inputColors.warning.border,
};
}
} else {
return {};
}
}

export default function errorRing(error?: ErrorType, theme?: CanvasTheme): CSSObject {
if (error !== ErrorType.Error && error !== ErrorType.Alert) {
return {};
}
const errorColors = getErrorColors(error, theme);
const errorBoxShadow = `inset 0 0 0 ${errorColors.outer === errorColors.inner ? 1 : 2}px ${
errorColors.inner
}`;

return {
borderColor: errorBorderColor,
borderColor: errorColors.outer,
transition: '100ms box-shadow',
boxShadow: errorBoxShadow,
'&:hover, &:disabled': {
borderColor: errorBorderColor,
borderColor: errorColors.outer,
},
'&:focus:not([disabled])': {
borderColor: errorBorderColor,
borderColor: errorColors.outer,
boxShadow: `${errorBoxShadow},
0 0 0 2px ${colors.frenchVanilla100},
0 0 0 4px ${theme ? theme.palette.common.focusOutline : inputColors.focusBorder}`,
Expand Down
149 changes: 79 additions & 70 deletions modules/form-field/react/stories/stories_Radio.tsx
Expand Up @@ -7,6 +7,7 @@ import {
ControlledComponentWrapper,
ComponentStatesTable,
permutateProps,
customColorTheme,
} from '../../../../utils/storybook';

import {Radio, RadioGroup} from '../../../radio/react';
Expand Down Expand Up @@ -159,75 +160,83 @@ storiesOf('Components|Inputs/Radio/React/Left Label/Radio', module)
</FormField>
));

storiesOf('Components|Inputs/Radio/React/Visual', module)
const RadioStates = () => (
<div>
<h3>Radio</h3>
<StaticStates>
<ComponentStatesTable
rowProps={permutateProps({
checked: [{value: true, label: 'Checked'}, {value: false, label: 'Unchecked'}],
})}
columnProps={permutateProps(
{
className: [
{label: 'Default', value: ''},
{label: 'Hover', value: 'hover'},
{label: 'Focus', value: 'focus'},
{label: 'Focus Hover', value: 'focus hover'},
{label: 'Active', value: 'active'},
{label: 'Active Hover', value: 'active hover'},
],
disabled: [{label: '', value: false}, {label: 'Disabled', value: true}],
},
props => {
if (props.disabled && !['', 'hover'].includes(props.className)) {
return false;
}
return true;
}
)}
>
{props => (
<Radio
{...props}
onChange={() => {}} // eslint-disable-line no-empty-function
label="Radio"
/>
)}
</ComponentStatesTable>
</StaticStates>
<h3>Radio Group</h3>
<StaticStates>
<ComponentStatesTable
rowProps={permutateProps({
error: [
{value: undefined, label: 'No Error'},
{value: FormField.ErrorType.Alert, label: 'Alert'},
{value: FormField.ErrorType.Error, label: 'Error'},
],
})}
columnProps={[{label: 'Default', props: {}}]}
>
{props => (
<FormField
useFieldset={true}
hintText={props.error ? hintText : undefined}
hintId={hintId}
labelPosition={FormField.LabelPosition.Left}
{...props}
>
<ControlledComponentWrapper>
<RadioGroup name="contact">
<Radio id="1" value="email" label="E-mail" />
<Radio id="2" value="fax" label="Fax (disabled)" disabled={true} />
</RadioGroup>
</ControlledComponentWrapper>
</FormField>
)}
</ComponentStatesTable>
</StaticStates>
</div>
);

storiesOf('Components|Inputs/Radio/React/Visual Testing', module)
.addParameters({component: Radio})
.addDecorator(withReadme(README))
.add('States', () => (
<div>
<h3>Radio</h3>
<StaticStates>
<ComponentStatesTable
rowProps={permutateProps({
checked: [{value: true, label: 'Checked'}, {value: false, label: 'Unchecked'}],
})}
columnProps={permutateProps(
{
className: [
{label: 'Default', value: ''},
{label: 'Hover', value: 'hover'},
{label: 'Focus', value: 'focus'},
{label: 'Focus Hover', value: 'focus hover'},
{label: 'Active', value: 'active'},
{label: 'Active Hover', value: 'active hover'},
],
disabled: [{label: '', value: false}, {label: 'Disabled', value: true}],
},
props => {
if (props.disabled && !['', 'hover'].includes(props.className)) {
return false;
}
return true;
}
)}
>
{props => (
<Radio
{...props}
onChange={() => {}} // eslint-disable-line no-empty-function
label="Radio"
/>
)}
</ComponentStatesTable>
</StaticStates>
<h3>Radio Group</h3>
<StaticStates>
<ComponentStatesTable
rowProps={permutateProps({
error: [
{value: undefined, label: 'No Error'},
{value: FormField.ErrorType.Alert, label: 'Alert'},
{value: FormField.ErrorType.Error, label: 'Error'},
],
})}
columnProps={[{label: 'Default', props: {}}]}
>
{props => (
<FormField
useFieldset={true}
hintText={props.error ? hintText : undefined}
hintId={hintId}
labelPosition={FormField.LabelPosition.Left}
{...props}
>
<ControlledComponentWrapper>
<RadioGroup name="contact">
<Radio id="1" value="email" label="E-mail" />
<Radio id="2" value="fax" label="Fax (disabled)" disabled={true} />
</RadioGroup>
</ControlledComponentWrapper>
</FormField>
)}
</ComponentStatesTable>
</StaticStates>
</div>
));
.add('States', () => <RadioStates />)
.addParameters({
canvasProviderDecorator: {
theme: customColorTheme,
},
})
.add('Theming', () => <RadioStates />);

0 comments on commit f268170

Please sign in to comment.