From 1d9417eb2e16487b074a8c330e741e014547d94f Mon Sep 17 00:00:00 2001 From: sushma1031 Date: Wed, 18 Oct 2023 21:50:56 +0530 Subject: [PATCH 1/9] Add namespace for contrast checker tool --- i18n/en/colors.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/i18n/en/colors.json b/i18n/en/colors.json index 7f121ea..516901c 100644 --- a/i18n/en/colors.json +++ b/i18n/en/colors.json @@ -12,5 +12,9 @@ "loss": "Loss", "regenerate": "Regenerate", "selectedColor": "Selected Color", - "transformedColor": "Transformed Color" + "transformedColor": "Transformed Color", + "foreground": "FOREGROUND", + "background": "BACKGROUND", + "contrastChecker": "Contrast Checker", + "contrastCheckerDescription": "Use this to test accessibility of text colors over background colors." } \ No newline at end of file From b5a267d3a91f54638d6c998b07b7c534964abce8 Mon Sep 17 00:00:00 2001 From: sushma1031 Date: Sat, 21 Oct 2023 22:47:55 +0530 Subject: [PATCH 2/9] Add tool to calculate text contrast --- components/colors/TextContrastChecker.tsx | 327 ++++++++++++++++++++++ pages/colors.tsx | 7 + 2 files changed, 334 insertions(+) create mode 100644 components/colors/TextContrastChecker.tsx diff --git a/components/colors/TextContrastChecker.tsx b/components/colors/TextContrastChecker.tsx new file mode 100644 index 0000000..5a6f11a --- /dev/null +++ b/components/colors/TextContrastChecker.tsx @@ -0,0 +1,327 @@ +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import ContentPasteGoIcon from '@mui/icons-material/ContentPasteGo'; +import { TextField, Typography } from '@mui/material'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import * as convert from 'colors-convert'; +import { HEX, HSL, RGB } from 'colors-convert/dist/cjs/lib/types/types'; +import React, { ChangeEvent, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; + +import useLocalState from '../../hooks/useLocalState'; +import useSupportsClipboardRead from '../../hooks/useSupportsClipboardRead'; + +export default function TextContrastChecker({ + serializeColor, + setToastMessage, + setToastOpen, + setToastSeverity, +}: { + serializeColor: (value: HEX | HSL | RGB) => string; + setToastMessage: (message: string) => void; + setToastOpen: (open: boolean) => void; + setToastSeverity: (severity: 'success' | 'error') => void; +}) { + const { t } = useTranslation(['colors', 'common']); + + const supportsClipboardRead = useSupportsClipboardRead(); + + const [fgColor, setFgColor] = useLocalState({ + key: 'contrastChecker_fgColor', + defaultValue: 'ffffff', + }); + const [fgErr, setFgErr] = useLocalState({ + key: 'colorPicker_fgErr', + defaultValue: false, + }); + const [bgColor, setBgColor] = useLocalState({ + key: 'contrastChecker_bgColor', + defaultValue: '000000', + }); + const [bgErr, setBgErr] = useLocalState({ + key: 'colorPicker_bgErr', + defaultValue: false, + }); + const [textContrast, setTextContrast] = useLocalState({ + key: 'contastChecker_textContrast', + defaultValue: '1', + }); + const [contrastErr, setContrastErr] = useLocalState({ + key: 'contastChecker_err', + defaultValue: '', + }); + + const HEXColorRegExp = /^([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; + const isValid = (color: string): boolean => + HEXColorRegExp.test(color); + + function HandleFgChange(hex: string) { + setFgColor(serializeColor(hex)); + if (/^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(hex)) { + setFgErr(false); + } else { + setContrastErr(t('colors:invalidHex')); + setFgErr(true); + } + } + + function HandleBgChange(hex: string) { + setBgColor(serializeColor(hex)); + if (/^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(hex)) { + setBgErr(false); + } else { + setContrastErr(t('colors:invalidHex')); + setBgErr(true); + } + } + + const updateColors = ( + eventData: + | ChangeEvent + | { target: { name: string; value: string } }, + ) => { + try { + setContrastErr(''); + const { name, value } = eventData.target; + switch (name) { + case 'foreground': + HandleFgChange(value); + break; + case 'background': + HandleBgChange(value); + break; + default: + break; + } + } catch (e: unknown) { + if (e instanceof Error) { + setContrastErr(e.message); + } + } + }; + + function lineariseRGB(rgb: number[]): number[] { + return rgb.map((channel) => + channel <= 0.04045 + ? channel / 12.92 + : ((channel + 0.055) / 1.055) ** 2.4, + ); + } + + function calculateLuminance(value: RGB): number { + const gammaEncoded = Object.values(value).map((v) => v / 255); + const [r, g, b] = lineariseRGB(gammaEncoded); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + } + + function calculateContrast(hexValues: string[]) { + if (!hexValues.every(isValid)) { + return; + } + + const [L1, L2] = hexValues + .map((value) => calculateLuminance(convert.hexToRgb(`#${value}`))) + .sort((l1, l2) => l2 - l1); + const contrast = (L1 + 0.05) / (L2 + 0.05); + setTextContrast(contrast.toFixed(2)); + } + + useEffect(() => { + calculateContrast([fgColor, bgColor]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fgColor, bgColor]); + + return ( + <> + + {t('colors:contrastChecker')} + + + {t('colors:contrastCheckerDescription')} + + + + + + + + {!!supportsClipboardRead && ( + + )} + + + + + + + {!!supportsClipboardRead && ( + + )} + + + + {contrastErr} + + + + + contrast ratio: + + {`${textContrast}:1`} + + + + + + ); +} diff --git a/pages/colors.tsx b/pages/colors.tsx index 51b8329..dd14423 100644 --- a/pages/colors.tsx +++ b/pages/colors.tsx @@ -16,6 +16,7 @@ import useEyeDropper from 'use-eye-dropper'; import FilterGenerator from '../components/colors/FilterGenerator'; import PreviewPane from '../components/colors/PreviewPane'; +import TextContrastChecker from '../components/colors/TextContrastChecker'; import Layout from '../components/Layout'; import Toast, { ToastProps } from '../components/Toast'; import useLocalState from '../hooks/useLocalState'; @@ -444,6 +445,12 @@ export default function Colors() { setToastSeverity={setToastSeverity} setToastOpen={setToastOpen} /> + Date: Thu, 26 Oct 2023 15:03:13 +0530 Subject: [PATCH 3/9] Add contrast preview Update namespace --- components/colors/ContrastPreview.tsx | 264 ++++++++++++++++++++++ components/colors/TextContrastChecker.tsx | 7 + i18n/en/colors.json | 13 +- 3 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 components/colors/ContrastPreview.tsx diff --git a/components/colors/ContrastPreview.tsx b/components/colors/ContrastPreview.tsx new file mode 100644 index 0000000..1162301 --- /dev/null +++ b/components/colors/ContrastPreview.tsx @@ -0,0 +1,264 @@ +import DoneIcon from '@mui/icons-material/Done'; +import { Stack, Typography } from '@mui/material'; +import Box from '@mui/material/Box'; +import Chip from '@mui/material/Chip'; +import Grid from '@mui/material/Grid'; +import { styled } from '@mui/material/styles'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +export default function ContrastPreview({ + contrast, + foreground, + background, +}: { + contrast: string; + foreground: string; + background: string; +}) { + const { t } = useTranslation(['colors', 'common']); + + const contrastValue = parseFloat(contrast) || 0; + + const AANormal = contrastValue >= 4.5; + const AALarge = contrastValue >= 3; + const AAGui = contrastValue >= 3; + const AAANormal = contrastValue >= 7; + const AAALarge = contrastValue >= 4.5; + + const TextInput = styled('input')({ + border: '2px solid', + borderColor: foreground, + color: '#fff', + padding: '5px', + backgroundColor: '#121212', + }); + + return ( + + + + + {t('colors:normalText')} + + + + + + {t('colors:wcagAA')} + + + + + {t('colors:wcagAAA')} + + + + + + + + {t('colors:contrastTestString')} + + + + + {t('colors:largeText')} + + + + + + {t('colors:wcagAA')} + + + + + {t('colors:wcagAAA')} + + + + + + + + + {t('colors:contrastTestString')} + + + + + + {t('colors:gui')} + + + + + + {t('colors:wcagAA')} + + + + + + + + + + + + + ); +} diff --git a/components/colors/TextContrastChecker.tsx b/components/colors/TextContrastChecker.tsx index 5a6f11a..4f34baf 100644 --- a/components/colors/TextContrastChecker.tsx +++ b/components/colors/TextContrastChecker.tsx @@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next'; import useLocalState from '../../hooks/useLocalState'; import useSupportsClipboardRead from '../../hooks/useSupportsClipboardRead'; +import ContrastPreview from './ContrastPreview'; export default function TextContrastChecker({ serializeColor, @@ -302,6 +303,7 @@ export default function TextContrastChecker({ border='1px solid #494949' minWidth='250px' minHeight='250px' + mb={4} sx={{ '& pre': { fontsize: '1rem', @@ -322,6 +324,11 @@ export default function TextContrastChecker({ + ); } diff --git a/i18n/en/colors.json b/i18n/en/colors.json index 516901c..e99c7fb 100644 --- a/i18n/en/colors.json +++ b/i18n/en/colors.json @@ -13,8 +13,17 @@ "regenerate": "Regenerate", "selectedColor": "Selected Color", "transformedColor": "Transformed Color", + "contrastChecker": "Contrast Checker", + "contrastCheckerDescription": "Use this to test accessibility of text colors over background colors.", "foreground": "FOREGROUND", "background": "BACKGROUND", - "contrastChecker": "Contrast Checker", - "contrastCheckerDescription": "Use this to test accessibility of text colors over background colors." + "wcagAA": "WCAG AA:", + "wcagAAA": "WCAG AAA:", + "normalText": "Normal Text", + "largeText": "Large Text", + "gui": "Graphical Objects and User Interface Components", + "contrastTestString": "The five boxing wizards jump quickly.", + "textInput": "Text Input", + "pass": "Pass", + "fail": "Fail" } \ No newline at end of file From 111bff328ff1ecbc6bf0ff6715f50eb5548eae01 Mon Sep 17 00:00:00 2001 From: sushma1031 Date: Thu, 26 Oct 2023 21:37:37 +0530 Subject: [PATCH 4/9] Add color swap functionality Update color change handling functions --- components/colors/TextContrastChecker.tsx | 26 +++++++++++++++++------ i18n/en/colors.json | 1 + 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/components/colors/TextContrastChecker.tsx b/components/colors/TextContrastChecker.tsx index 4f34baf..86d9467 100644 --- a/components/colors/TextContrastChecker.tsx +++ b/components/colors/TextContrastChecker.tsx @@ -1,5 +1,6 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import ContentPasteGoIcon from '@mui/icons-material/ContentPasteGo'; +import SwapVertIcon from '@mui/icons-material/SwapVert'; import { TextField, Typography } from '@mui/material'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; @@ -45,20 +46,20 @@ export default function TextContrastChecker({ }); const [textContrast, setTextContrast] = useLocalState({ key: 'contastChecker_textContrast', - defaultValue: '1', + defaultValue: '21', }); const [contrastErr, setContrastErr] = useLocalState({ key: 'contastChecker_err', defaultValue: '', }); - const HEXColorRegExp = /^([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; + const HEXColorRegExp = /^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; const isValid = (color: string): boolean => HEXColorRegExp.test(color); function HandleFgChange(hex: string) { setFgColor(serializeColor(hex)); - if (/^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(hex)) { + if (isValid(hex)) { setFgErr(false); } else { setContrastErr(t('colors:invalidHex')); @@ -68,7 +69,7 @@ export default function TextContrastChecker({ function HandleBgChange(hex: string) { setBgColor(serializeColor(hex)); - if (/^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(hex)) { + if (isValid(hex)) { setBgErr(false); } else { setContrastErr(t('colors:invalidHex')); @@ -76,6 +77,13 @@ export default function TextContrastChecker({ } } + function handleColorSwap() { + if (isValid(fgColor) && isValid(bgColor)) { + HandleBgChange(fgColor); + HandleFgChange(bgColor); + } + } + const updateColors = ( eventData: | ChangeEvent @@ -158,7 +166,7 @@ export default function TextContrastChecker({ display='flex' flexDirection='column' justifyContent='stretch' - gap={7} + gap={2} > + Date: Thu, 26 Oct 2023 22:34:24 +0530 Subject: [PATCH 5/9] Update namespace Add aria-label to swap button --- components/colors/TextContrastChecker.tsx | 3 ++- i18n/en/colors.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/components/colors/TextContrastChecker.tsx b/components/colors/TextContrastChecker.tsx index 86d9467..ac74e14 100644 --- a/components/colors/TextContrastChecker.tsx +++ b/components/colors/TextContrastChecker.tsx @@ -231,10 +231,11 @@ export default function TextContrastChecker({ Date: Thu, 26 Oct 2023 22:37:11 +0530 Subject: [PATCH 6/9] Add contrast tool test --- __TESTS__/colors.spec.tsx | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/__TESTS__/colors.spec.tsx b/__TESTS__/colors.spec.tsx index 0db5dcf..d67d7ed 100644 --- a/__TESTS__/colors.spec.tsx +++ b/__TESTS__/colors.spec.tsx @@ -51,4 +51,43 @@ describe('Colors', () => { expect(hsl).toHaveValue('333, 55, 51'); expect(hex).toHaveValue('C9397B'); }); + + it('reacts to foreground color change correctly', async () => { + const user = userEvent.setup(); + render(); + + const fg = screen.getByLabelText('colors:foreground'); + + await user.clear(fg); + await user.type(fg, '232c34'); + expect(fg).toHaveValue('232c34'); + }); + + it('reacts to background color change correctly', async () => { + const user = userEvent.setup(); + render(); + + const bg = screen.getByLabelText('colors:background'); + + await user.clear(bg); + await user.type(bg, '232c34'); + expect(bg).toHaveValue('232c34'); + }); + + it('reacts to color swap correctly', async () => { + const user = userEvent.setup(); + render(); + + const bg = screen.getByLabelText('colors:background'); + const fg = screen.getByLabelText('colors:foreground'); + const swap = screen.getByLabelText('colors:swapColors'); + + await user.clear(fg); + await user.clear(bg); + await user.type(fg, '232c34'); + await user.type(bg, 'c9397b'); + await userEvent.click(swap); + expect(fg).toHaveValue('c9397b'); + expect(bg).toHaveValue('232c34'); + }); }); From db8fcf6a35393d50a5edd15612228637706589f8 Mon Sep 17 00:00:00 2001 From: sushma1031 Date: Thu, 26 Oct 2023 22:39:44 +0530 Subject: [PATCH 7/9] Update changelog.yml --- data/changelog.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/changelog.yml b/data/changelog.yml index bdfdf92..dffbb63 100644 --- a/data/changelog.yml +++ b/data/changelog.yml @@ -4,6 +4,9 @@ # | Date | Time | Timezone (GMT -7 hours in this example) # 2022-10-26T00:00:00-0700 +- date: 2023-10-26T00:00:00+0530 + note: Added color contrast checker tool + - date: 2022-12-10T00:00:00-0700 note: Added CSS color transform filter generator to color tool From 6a436f612a72b257df47d16284be94322845e0f5 Mon Sep 17 00:00:00 2001 From: sushma1031 Date: Sat, 28 Oct 2023 21:31:18 +0530 Subject: [PATCH 8/9] Update translations --- components/colors/TextContrastChecker.tsx | 2 +- i18n/en/colors.json | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/components/colors/TextContrastChecker.tsx b/components/colors/TextContrastChecker.tsx index ac74e14..c51f7b0 100644 --- a/components/colors/TextContrastChecker.tsx +++ b/components/colors/TextContrastChecker.tsx @@ -327,7 +327,7 @@ export default function TextContrastChecker({ }, }} > - contrast ratio: + {t('colors:contrastRatio')} Date: Sat, 28 Oct 2023 21:53:32 +0530 Subject: [PATCH 9/9] Update style --- components/colors/ContrastPreview.tsx | 9 ++++++--- components/colors/TextContrastChecker.tsx | 21 +++++++++++++-------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/components/colors/ContrastPreview.tsx b/components/colors/ContrastPreview.tsx index 1162301..9dca245 100644 --- a/components/colors/ContrastPreview.tsx +++ b/components/colors/ContrastPreview.tsx @@ -55,7 +55,8 @@ export default function ContrastPreview({ > {t('colors:normalText')} @@ -126,7 +127,8 @@ export default function ContrastPreview({ > {t('colors:largeText')} @@ -202,7 +204,8 @@ export default function ContrastPreview({ > {t('colors:gui')} diff --git a/components/colors/TextContrastChecker.tsx b/components/colors/TextContrastChecker.tsx index c51f7b0..54395f0 100644 --- a/components/colors/TextContrastChecker.tsx +++ b/components/colors/TextContrastChecker.tsx @@ -230,13 +230,18 @@ export default function TextContrastChecker({ )} - + + {`${textContrast}:1`}