Skip to content

Commit

Permalink
add color contrast checker component
Browse files Browse the repository at this point in the history
  • Loading branch information
danalvrz committed Jan 30, 2025
1 parent 21bb97d commit 276443f
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import type { Content } from '@plone/types';
import cx from 'classnames';
import config from '@plone/volto/registry';

type FormState = {
content: {
data: Content;
};
form: {
global: Content;
};
};

const ColorContrastChecker = (props: { id: string; value: string }) => {
const { id, value } = props;
const [backgroundColor, setBackgroundColor] = useState('#ffffff');
const [foregroundColor, setForegroundColor] = useState('#000000');
const [contrastRatio, setContrastRatio] = useState(21);

const formData = useSelector<FormState, Content>(
(state) => state.form.global,
);
const colorPairMap = config.settings.colorContrastPairMap;

// Convert hex to RGB
const hexToRgb = (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
};

// Calculate relative luminance
const getLuminance = (r, g, b) => {
const [rs, gs, bs] = [r, g, b].map((c) => {
c = c / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
};

// Calculate contrast ratio
const getContrastRatio = (l1, l2) => {
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
};

useEffect(() => {
const bg = hexToRgb(backgroundColor);
const fg = hexToRgb(foregroundColor);
if (bg && fg) {
const bgLum = getLuminance(bg.r, bg.g, bg.b);
const fgLum = getLuminance(fg.r, fg.g, fg.b);
const ratio = getContrastRatio(bgLum, fgLum);
setContrastRatio(ratio);
}

const colorPair = formData[colorPairMap[id]]?.toString();
const newColorHex = value?.toString();

if (id.includes('foreground')) {
setForegroundColor(newColorHex);
setBackgroundColor(colorPair ?? backgroundColor);
} else {
setForegroundColor(colorPair ?? foregroundColor);
setBackgroundColor(newColorHex);
}
}, [backgroundColor, colorPairMap, foregroundColor, formData, id, value]);

// Get WCAG compliance levels
const getComplianceLevel = (ratio) => {
if (ratio >= 3) return 'AA Large';
return 'Failed';
};

return (
<>
{formData[id] && contrastRatio < 4.5 && (
<span
className={cx('color-contrast-label')}
role="alert"
aria-live="polite"
>
The color contrast ratio ({contrastRatio.toFixed(2)}:1) might not be
accesible for all. WCAG Level: {getComplianceLevel(contrastRatio)}
<a
target="_blank"
href="https://webaim.org/articles/contrast/"
rel="noreferrer"
>
&#x3F;
</a>
</span>
)}
</>
);
};

export default ColorContrastChecker;
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWr
import { HexColorPicker, HexColorInput } from 'react-colorful';
import { Button, Dialog, DialogTrigger, Popover } from 'react-aria-components';
import { ColorSwatch, CloseIcon } from '@plone/components';
import ColorContrastChecker from './ColorContrastChecker';

const ColorPickerWidget = (props: {
id: string;
Expand All @@ -13,40 +14,43 @@ const ColorPickerWidget = (props: {
const { id, onChange, value } = props;

return (
<FormFieldWrapper {...props} className="theme-color-picker">
<DialogTrigger>
<Button className="theme-color-picker-button">
<ColorSwatch color={value || '#fff'} />
</Button>
<>
<FormFieldWrapper {...props} className="theme-color-picker">
<DialogTrigger>
<Button className="theme-color-picker-button">
<ColorSwatch color={value || '#fff'} />
</Button>

<Popover placement="bottom start">
<Dialog className="theme-color-picker-dialog">
<HexColorPicker
color={value || ''}
onChange={(value) => {
// edge case for Batman value
if (value !== '#NaNNaNNaN') {
onChange(id, value);
}
}}
/>
</Dialog>
</Popover>
</DialogTrigger>
<HexColorInput
color={value || ''}
onChange={(value) => onChange(id, value)}
prefixed
/>
<Button
className="theme-color-picker-reset react-aria-Button"
onPress={() => {
onChange(id, '');
}}
>
<CloseIcon size="S" />
</Button>
</FormFieldWrapper>
<Popover placement="bottom start">
<Dialog className="theme-color-picker-dialog">
<HexColorPicker
color={value || ''}
onChange={(value) => {
// edge case for Batman value
if (value !== '#NaNNaNNaN') {
onChange(id, value);
}
}}
/>
</Dialog>
</Popover>
</DialogTrigger>
<HexColorInput
color={value || ''}
onChange={(value) => onChange(id, value)}
prefixed
/>
<Button
className="theme-color-picker-reset react-aria-Button"
onPress={() => {
onChange(id, '');
}}
>
<CloseIcon size="S" />
</Button>
</FormFieldWrapper>
<ColorContrastChecker {...props} />
</>
);
};

Expand Down
9 changes: 9 additions & 0 deletions packages/volto-light-theme/src/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,14 @@ export default function install(config: ConfigType) {
'secondary_foreground_color',
];

config.settings.colorContrastPairMap = {
primary_color: 'primary_foreground_color',
primary_foreground_color: 'primary_color',
secondary_color: 'secondary_foreground_color',
secondary_foreground_color: 'secondary_color',
accent_color: 'accent_foreground_color',
accent_foreground_color: 'accent_color',
};

return config;
}
18 changes: 18 additions & 0 deletions packages/volto-light-theme/src/theme/_widgets.scss
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,24 @@
}
}

span.color-contrast-label {
color: #ed6500;
font-size: 13px;
display: block;
position: relative;
padding-left: 20%;
a {
padding: 0.1rem 0.25rem;
border: 1px solid var(--link-foreground-color);
border-radius: 100%;
margin: 0;
margin-left: 4px;
margin-left: 4px;
font-size: 0.4rem;
vertical-align: super;
}
}

.react-aria-ColorSwatch {
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.3);
}
Expand Down

0 comments on commit 276443f

Please sign in to comment.