From 695f61ffc5eae8c63d1c38f7a1d80ea04a7b3d29 Mon Sep 17 00:00:00 2001 From: Misha Moroshko Date: Fri, 17 Jan 2020 15:33:17 +1100 Subject: [PATCH] Add contrast and controls to Color Contrast Matrix --- src/components/Container.js | 4 + src/components/Container.test.js | 20 +++ src/components/Input.js | 4 +- src/components/Select.js | 11 +- src/hooks/useResponsiveProp.js | 5 + src/themes/default/input.js | 2 +- src/themes/default/select.js | 4 +- src/utils/css.js | 8 + src/utils/css.test.js | 13 ++ website/src/layouts/Page.js | 12 +- website/src/pages/colors/accessibility.js | 199 ++++++++++++++++------ website/src/pages/colors/index.js | 6 +- website/src/themes/website/field.js | 26 +++ website/src/themes/website/index.js | 8 +- website/src/themes/website/input.js | 37 ++++ website/src/themes/website/select.js | 47 +++++ website/src/utils/color.js | 34 ++++ website/src/utils/color.test.js | 10 ++ 18 files changed, 388 insertions(+), 62 deletions(-) create mode 100644 website/src/themes/website/field.js create mode 100644 website/src/themes/website/input.js create mode 100644 website/src/themes/website/select.js create mode 100644 website/src/utils/color.js create mode 100644 website/src/utils/color.test.js diff --git a/src/components/Container.js b/src/components/Container.js index 46181ea5..bfca0b7c 100644 --- a/src/components/Container.js +++ b/src/components/Container.js @@ -5,12 +5,14 @@ import { ContainerProvider } from "../hooks/useContainer"; import { responsiveMarginType, responsivePaddingType, + responsiveWidthType, responsiveHeightType } from "../hooks/useResponsiveProp"; import useResponsivePropsCSS from "../hooks/useResponsivePropsCSS"; import { responsiveMargin, responsivePadding, + responsiveWidth, responsiveHeight, mergeResponsiveCSS } from "../utils/css"; @@ -37,6 +39,7 @@ function Container(_props) { const responsivePropsCSS = useResponsivePropsCSS(props, { margin: responsiveMargin, padding: responsivePadding, + width: responsiveWidth, height: responsiveHeight }); const responsiveCSS = hasBreakpointWidth @@ -98,6 +101,7 @@ Container.propTypes = { boxShadow: PropTypes.oneOf(BOX_SHADOWS), ...responsiveMarginType, ...responsivePaddingType, + ...responsiveWidthType, ...responsiveHeightType, hasBreakpointWidth: PropTypes.bool, children: PropTypes.node diff --git a/src/components/Container.test.js b/src/components/Container.test.js index b0ab9b77..89d99528 100644 --- a/src/components/Container.test.js +++ b/src/components/Container.test.js @@ -33,6 +33,26 @@ describe("Container", () => { `); }); + it("with width", () => { + const { getByText } = render(Hello World); + const node = getByText("Hello World"); + + expect(node).toHaveStyle(` + width: 80px; + `); + }); + + it("with height", () => { + const { getByText } = render( + Hello World + ); + const node = getByText("Hello World"); + + expect(node).toHaveStyle(` + height: 72px; + `); + }); + it("with background color", () => { const { getByText } = render( Hello World diff --git a/src/components/Input.js b/src/components/Input.js index 09bbe60e..18e107f7 100644 --- a/src/components/Input.js +++ b/src/components/Input.js @@ -51,6 +51,7 @@ function Input(_props) { const { inputColor } = useContainer(); const color = !COLORS.includes(_props.color) && inputColor ? inputColor : props.color; + const colorStr = color === DEFAULT_PROPS.color ? "default" : color; const [inputId] = useState(() => `input-${nanoid()}`); const [auxId] = useState(() => `input-aux-${nanoid()}`); const [isTouched, setIsTouched] = useState(false); @@ -75,8 +76,9 @@ function Input(_props) { `select-${nanoid()}`); const [auxId] = useState(() => `select-aux-${nanoid()}`); const [isTouched, setIsTouched] = useState(false); @@ -81,11 +82,17 @@ function Select(_props) { + + + + ); } diff --git a/website/src/pages/colors/index.js b/website/src/pages/colors/index.js index cb8ae439..8b0ce97d 100644 --- a/website/src/pages/colors/index.js +++ b/website/src/pages/colors/index.js @@ -15,7 +15,11 @@ function ColorGroup({ title, subTitle, children }) { paddingTop: designTokens.space[2] }} > - {title && {title}} + {title && ( + + {title} + + )} {subTitle && (
{subTitle} diff --git a/website/src/themes/website/field.js b/website/src/themes/website/field.js new file mode 100644 index 00000000..b51700bb --- /dev/null +++ b/website/src/themes/website/field.js @@ -0,0 +1,26 @@ +import { designTokens as tokens } from "basis"; + +export default { + field: { + display: "inline-flex", + flexDirection: "column", + position: "relative" + }, + "field.fullWidth": { + display: "flex", + width: "100%", + minWidth: 0 // See: https://stackoverflow.com/a/36247448/247243 + }, + "field.disabled": { + opacity: 0.5 + }, + "field.label": { + display: "flex", + fontFamily: tokens.fonts.body, + fontSize: tokens.fontSizes[0], + fontWeight: tokens.fontWeights.medium, + lineHeight: tokens.lineHeights[1], + color: tokens.colors.grey.t65, + marginBottom: tokens.space[1] + } +}; diff --git a/website/src/themes/website/index.js b/website/src/themes/website/index.js index 090d93ba..0a3f47c6 100644 --- a/website/src/themes/website/index.js +++ b/website/src/themes/website/index.js @@ -1,9 +1,15 @@ import { defaultTheme } from "basis"; import button from "./button"; +import field from "./field"; +import input from "./input"; +import select from "./select"; const websiteTheme = { ...defaultTheme, - ...button + ...button, + ...field, + ...input, + ...select }; export default websiteTheme; diff --git a/website/src/themes/website/input.js b/website/src/themes/website/input.js new file mode 100644 index 00000000..ed1c67f2 --- /dev/null +++ b/website/src/themes/website/input.js @@ -0,0 +1,37 @@ +import { designTokens as tokens } from "basis"; + +export default { + input: { + boxSizing: "border-box", + fontSize: tokens.fontSizes[0], + fontWeight: tokens.fontWeights.light, + lineHeight: tokens.lineHeights[1], + fontFamily: tokens.fonts.body, + padding: `0 ${tokens.space[2]}`, + width: "100%", + height: tokens.space[8], + margin: 0, + borderRadius: tokens.radii[1], + borderWidth: tokens.borderWidths[0], + borderStyle: "solid", + MozAppearance: "textfield", // Hides the input="number" spin buttons in Firefox + transition: "color 100ms ease, border-color 100ms ease" + }, + "input:focus": { + outline: 0, + color: tokens.colors.black, + borderColor: tokens.colors.black + }, + "input:hover": { + color: tokens.colors.black, + borderColor: tokens.colors.black + }, + "input.webkitSpinButton": { + display: "none" // Hides the input="number" spin buttons in Chrome + }, + "input.default": { + color: tokens.colors.grey.t65, + backgroundColor: "transparent", + borderColor: tokens.colors.grey.t30 + } +}; diff --git a/website/src/themes/website/select.js b/website/src/themes/website/select.js new file mode 100644 index 00000000..3a9b58c3 --- /dev/null +++ b/website/src/themes/website/select.js @@ -0,0 +1,47 @@ +import { designTokens as tokens } from "basis"; + +export default { + selectInput: { + display: "inline-block", + fontSize: tokens.fontSizes[0], + lineHeight: tokens.lineHeights[0], + fontFamily: tokens.fonts.body, + fontWeight: tokens.fontWeights.light, + height: tokens.sizes[8], + paddingLeft: tokens.space[3], + paddingRight: tokens.space[9], + margin: 0, + borderRadius: tokens.radii[1], + borderWidth: tokens.borderWidths[0], + borderStyle: "solid", + appearance: "none", + backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32' role='img' aria-label='Triangle down'%3E%3Cpath d='M20.747 14.509l-4.181 4.25a.786.786 0 01-1.132 0l-4.179-4.247a.885.885 0 01-.231-.827c.07-.3.287-.536.569-.62.282-.084 8.607-.101 8.912.035a.86.86 0 01.495.802.874.874 0 01-.253.607z' fill='%23414141'%3E%3C/path%3E%3C/svg%3E")`, + backgroundRepeat: "no-repeat", + backgroundPosition: `right 0 top 50%`, + alignSelf: "flex-start", + transition: "color 100ms ease, border-color 100ms ease" + }, + "selectInput.fullWidth": { + alignSelf: "auto" + }, + "selectInput:focus": { + outline: 0 + }, + "selectInput.default": { + color: tokens.colors.grey.t65, + backgroundColor: "transparent", + borderColor: tokens.colors.grey.t30 + }, + "selectInput.default:focus": { + color: tokens.colors.black, + borderColor: tokens.colors.black + }, + "selectInput.default:hover": { + color: tokens.colors.black, + borderColor: tokens.colors.black + }, + "selectInput.default:active": { + color: tokens.colors.black, + borderColor: tokens.colors.black + } +}; diff --git a/website/src/utils/color.js b/website/src/utils/color.js new file mode 100644 index 00000000..f71cfad0 --- /dev/null +++ b/website/src/utils/color.js @@ -0,0 +1,34 @@ +function str2rgb(str) { + return { + r: parseInt(str.slice(1, 3), 16), + g: parseInt(str.slice(3, 5), 16), + b: parseInt(str.slice(5, 7), 16) + }; +} + +// http://www.w3.org/WAI/GL/wiki/Relative_luminance +function relativeLuminance({ r, g, b }) { + [r, g, b] = [r, g, b].map(c => { + c = c / 255; + + if (c <= 0.03928) { + return c / 12.92; + } + + return Math.pow((c + 0.055) / 1.055, 2.4); + }); + + return 0.2126 * r + 0.7152 * g + 0.0722 * b; +} + +// http://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html#key-terms +export function colorContrast(str1, str2) { + const L1 = relativeLuminance(str2rgb(str1)); + const L2 = relativeLuminance(str2rgb(str2)); + + if (L1 < L2) { + return (L2 + 0.05) / (L1 + 0.05); + } + + return (L1 + 0.05) / (L2 + 0.05); +} diff --git a/website/src/utils/color.test.js b/website/src/utils/color.test.js new file mode 100644 index 00000000..da83791b --- /dev/null +++ b/website/src/utils/color.test.js @@ -0,0 +1,10 @@ +import { colorContrast } from "./color"; + +describe("colorContrast", () => { + it("calculates the contrast", () => { + expect(colorContrast("#123456", "#123456")).toBe(1); + expect(colorContrast("#e7aa90", "#193ccb")).toBeCloseTo(4.137350318176988); + expect(colorContrast("#000000", "#ffffff")).toBe(21); + expect(colorContrast("#ffffff", "#000000")).toBe(21); + }); +});