[Bug]: a11y-addon - APCA Contrast check - Can't pass custom evaluate function to rule
peterlaxalt opened this issue · comments
Describe the bug
I am trying to integrate Stack Exchange's implementation of the APCA contrast check by extending the a11y
addon options. For now I've duplicated his script into a separate folder in the project and modified his function to not run axe.configure
but instead return the check
and rule
manually.
![Screen Shot 2024-06-19 at 3 29 30 PM](https://private-user-images.githubusercontent.com/15971341/341185983-c9fc37a6-1ed1-4972-8c9c-6c966a839610.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTk3NDc3OTQsIm5iZiI6MTcxOTc0NzQ5NCwicGF0aCI6Ii8xNTk3MTM0MS8zNDExODU5ODMtYzlmYzM3YTYtMWVkMS00OTcyLThjOWMtNmM5NjZhODM5NjEwLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA2MzAlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwNjMwVDExMzgxNFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWFiNjlmZGY5ZjQxZmMzZDM0ZTZjYWE3OWE4NzYyZTVmMDMzODBhZDM5ZTE4Y2Q5MmM5NGRkYWFiZTIwZWZhZWMmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0._qETC2LAQK8nofAHrsGODgWeTxLUOIGd2AGKhnb4Itk)
The axe
docs are a little wonky, and say that evaluate
accepts a string, but if you look into the code, the type does accept string or function.
I've got it working and showing up on Storybook in the a11y
addon, but the check
always comes back as 'Incomplete.' Are we passing the node
differently somehow? I've tried accessing the virtualNode.actualNode
as well with no luck. Just always returns 'Incomplete'. I can't even force it to return true
or false
as well. The APCA contrast check works on it's own with a manual axe
setup, but not inside of Storybook.
The updated function is below. Am I missing anything?
In preview.ts
import type { Preview } from "@storybook/react";
import '../src/assets/styles/globals.scss'
import '../public/main.latin.min.css'
import { registerAPCACheck } from '../vendor/apca-check/index.js';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: 'rgba(255,255,255,1.00)' },
{ name: 'dark', value: 'rgba(33,47,61,1.00)' },
],
},
a11y: {
config: {
checks: [
{ ... registerAPCACheck('silver-plus').check },
],
rules: [
{ ...registerAPCACheck('silver-plus').rule },
{ id: "color-contrast", enabled: false },
],
},
}
},
};
export default preview;
In apca-check.js
, just a copy + pase from the npm package. For apca-bronze.js
/ apca-silver-plus.js
, they are copy and pastes as well.
const apcaW3 = require("apca-w3");
const calcAPCA = apcaW3.calcAPCA;
const APCABronzeConformanceThresholdFn = require("./apca-bronze.js");
const APCASilverPlusConformanceThresholdFn = require("./apca-silver-plus.js");
const axe = require("axe-core");
const generateColorContrastAPCAConformanceCheck = (
conformanceLevel,
conformanceThresholdFn
) => {
return {
id: `color-contrast-apca-${conformanceLevel}-conformance`,
metadata: {
impact: "serious",
messages: {
pass:
"Element has sufficient APCA " +
conformanceLevel +
"level lightness contrast (Lc) of ${this.data.apcaContrast}Lc (foreground color: ${this.data.fgColor}, background color: ${this.data.bgColor}, font size: ${this.data.fontSize}, font weight: ${this.data.fontWeight}). Expected minimum APCA contrast of ${this.data.apcaThreshold}}",
fail: {
default:
"Element has insufficient APCA " +
conformanceLevel +
"level contrast of ${this.data.apcaContrast}Lc (foreground color: ${this.data.fgColor}, background color: ${this.data.bgColor}, font size: ${this.data.fontSize}, font weight: ${this.data.fontWeight}). Expected minimum APCA lightness contrast of ${this.data.apcaThreshold}Lc",
increaseFont:
"Element has insufficient APCA " +
conformanceLevel +
" level contrast of ${this.data.apcaContrast}Lc (foreground color: ${this.data.fgColor}, background color: ${this.data.bgColor}, font size: ${this.data.fontSize}, font weight: ${this.data.fontWeight}). Increase font size and/or font weight to meet APCA conformance minimums",
},
incomplete: "Unable to determine APCA lightness contrast (Lc)",
},
},
evaluate(node) {
const nodeStyle = window.getComputedStyle(node);
const fontSize = nodeStyle.getPropertyValue("font-size");
const fontWeight = nodeStyle.getPropertyValue("font-weight");
const bgColor = axe.commons.color.getBackgroundColor(node);
const fgColor = axe.commons.color.getForegroundColor(
node,
false,
bgColor
);
// missing data to determine APCA contrast for this node
if (!bgColor || !fgColor || !fontSize || !fontWeight) {
return undefined;
}
const toRGBA = (color) => {
return `rgba(${color.red}, ${color.green}, ${color.blue}, ${color.alpha})`;
};
const apcaContrast = Math.abs(calcAPCA(toRGBA(fgColor), toRGBA(bgColor)));
const apcaThreshold = conformanceThresholdFn(fontSize, fontWeight);
this.data = {
fgColor: fgColor.toHexString(),
bgColor: bgColor.toHexString(),
fontSize: `${((parseFloat(fontSize) * 72) / 96).toFixed(1)}pt (${parseFloat(fontSize)}px)`,
fontWeight: fontWeight,
apcaContrast: Math.round(apcaContrast * 100) / 100,
apcaThreshold: apcaThreshold,
messageKey: apcaThreshold === null ? "increaseFont" : "default",
};
return apcaThreshold ? apcaContrast >= apcaThreshold : void 0;
},
};
};
const generateColorContrastAPCARule = (conformanceLevel) => ({
id: `color-contrast-apca-${conformanceLevel}`,
impact: "serious",
matches: "color-contrast-matches",
metadata: {
help: "Elements must meet APCA conformance minimums thresholds",
description: `Ensures the contrast between foreground and background colors meets APCA ${conformanceLevel} level conformance minimums thresholds`,
helpUrl:
"https://readtech.org/ARC/tests/visual-readability-contrast/?tn=criterion",
},
all: [`color-contrast-apca-${conformanceLevel}-conformance`],
tags: ["apca", "wcag3", `apca-${conformanceLevel}`],
});
const registerAPCACheck = (conformanceLevel, customConformanceThresholdFn) => {
if (
conformanceLevel === "custom" &&
typeof customConformanceThresholdFn !== "function"
) {
throw new Error(
"A custom conformance level requires a custom conformance threshold function"
);
}
const conformanceThresholdFnMap = {
bronze: APCABronzeConformanceThresholdFn,
silver: APCASilverPlusConformanceThresholdFn,
custom: customConformanceThresholdFn,
};
return {
rule: generateColorContrastAPCARule(conformanceLevel),
check: generateColorContrastAPCAConformanceCheck(
conformanceLevel,
conformanceThresholdFnMap[conformanceLevel]
),
};
};
module.exports = { registerAPCACheck };
Reproduction link
https://github.com/StackExchange/apca-check
Reproduction steps
No response
System
Storybook Environment Info:
(node:81977) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
System:
OS: macOS 12.0.1
CPU: (8) x64 Apple M1
Shell: 5.8 - /bin/zsh
Binaries:
Node: 21.0.0 - ~/.nvm/versions/node/v21.0.0/bin/node
Yarn: 1.22.22 - ~/.nvm/versions/node/v21.0.0/bin/yarn
npm: 10.2.0 - ~/.nvm/versions/node/v21.0.0/bin/npm <----- active
pnpm: 9.4.0 - ~/.nvm/versions/node/v21.0.0/bin/pnpm
Browsers:
Chrome: 126.0.6478.62
Safari: 15.1
npmPackages:
@storybook/addon-a11y: ^8.1.10 => 8.1.10
@storybook/addon-backgrounds: ^8.1.9 => 8.1.9
@storybook/addon-console: ^3.0.0 => 3.0.0
@storybook/addon-essentials: ^8.1.9 => 8.1.9
@storybook/addon-interactions: ^8.1.9 => 8.1.9
@storybook/addon-links: ^8.1.9 => 8.1.9
@storybook/addon-onboarding: ^8.1.9 => 8.1.9
@storybook/addon-styling-webpack: ^1.0.0 => 1.0.0
@storybook/blocks: ^8.1.9 => 8.1.9
@storybook/nextjs: ^8.1.9 => 8.1.9
@storybook/react: ^8.1.9 => 8.1.9
@storybook/test: ^8.1.9 => 8.1.9
eslint-plugin-storybook: ^0.8.0 => 0.8.0
storybook: ^8.1.9 => 8.1.9
Additional context
No response