Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@
All notable changes to `@ngbracket/a11y-devtools` are documented here.
This project adheres to [Semantic Versioning](https://semver.org/).

## 0.15.7

### Fixed

- The keyboard tab-order badges now pass a colour contrast check. Their white
digits sat on a teal fill at 3.93:1, below the 4.5:1 that text this size
needs, and axe flagged them on a dark-mode page. The fill is now `#0a7a7a`
(5.15:1). The orange badge for a positive `tabindex` had the same problem
(3.09:1) and is now `#b45309` (5.02:1). The lines joining the badges keep
their teal.
- The finding labels now pass a colour contrast check too. Their white text
sat on the impact colour, which gave 2.42:1 to 4.11:1 for every impact except
critical. Labels now use a darker fill in the same hue: serious `#b45309`,
moderate `#8a6d00`, minor `#2563c4` and no impact `#666666`, all at least
4.9:1. The box outlines keep the original impact colours.

## 0.15.6

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngbracket/a11y-devtools",
"version": "0.15.6",
"version": "0.15.7",
"description": "Finds accessibility problems in Angular apps and names the component behind each one. A dev overlay, or headless reports for CI.",
"license": "MIT",
"author": "Duncan Faulkner",
Expand Down
25 changes: 21 additions & 4 deletions src/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ import type { AxDescription } from './keyboard/accname.js';
import { keepInTopLayer } from './top-layer.js';

const SVG_NS = 'http://www.w3.org/2000/svg';
/** Tab-order path + badge colours: normal teal, warning orange for positive tabindex. */
/** Tab-order connector line colour (teal). */
const TAB_ORDER_COLOR = '#0b8f8f';
const TAB_ORDER_WARN_COLOR = '#e8710a';
/**
* Badge fills behind white 10px digits: at least 4.5:1 with white (teal 5.15,
* orange 5.02). The orange marks a positive-tabindex stop.
*/
const TAB_BADGE_FILL = '#0a7a7a';
const TAB_BADGE_WARN_FILL = '#b45309';
/** How far outside a control's left edge the tab-order badge sits, and its min viewport x. */
const TAB_BADGE_GUTTER = 10;
const TAB_BADGE_MIN_X = 9;
Expand All @@ -31,6 +36,18 @@ const IMPACT_COLOR: Record<NonNullable<Impact> | 'none', string> = {
none: '#8a8a8a',
};

/**
* Label fill per impact, behind white 11px text: at least 4.5:1 with white, in
* the same hue as the border, so the labels pass a contrast check themselves.
*/
const IMPACT_LABEL_FILL: Record<NonNullable<Impact> | 'none', string> = {
critical: '#d32029',
serious: '#b45309',
moderate: '#8a6d00',
minor: '#2563c4',
none: '#666666',
};

function colorFor(impact: Impact): string {
return IMPACT_COLOR[impact ?? 'none'];
}
Expand Down Expand Up @@ -340,7 +357,7 @@ function buildAxPanelContent(doc: Document, data: AxPanelData): HTMLElement[] {
* positive tabindex hijacks the natural order.
*/
function buildBadge(doc: Document, stop: TabStop): HTMLElement {
const color = stop.positive ? TAB_ORDER_WARN_COLOR : TAB_ORDER_COLOR;
const color = stop.positive ? TAB_BADGE_WARN_FILL : TAB_BADGE_FILL;
const badge = doc.createElement('div');
badge.setAttribute('data-ngb-tab-order', String(stop.order));
const owner = stop.component ? ` · ${stop.component}` : '';
Expand Down Expand Up @@ -402,7 +419,7 @@ function buildBox(doc: Document, finding: A11yFinding): HTMLElement {
padding: '1px 4px',
font: '11px/1.4 ui-monospace, monospace',
color: '#fff',
background: color,
background: IMPACT_LABEL_FILL[finding.impact ?? 'none'],
whiteSpace: 'nowrap',
});
box.appendChild(label);
Expand Down
39 changes: 38 additions & 1 deletion src/testing/overlay.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ function overlayRoot(): HTMLElement | null {
return document.querySelector(`[${OVERLAY_ATTR}]`);
}

/** WCAG contrast of an `rgb(r, g, b)` fill against white text. */
function contrastWithWhite(fill: string): number {
const lum = fill
.match(/\d+/g)!
.slice(0, 3)
.map((v) => Number(v) / 255)
.map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4))
.reduce((sum, v, i) => sum + v * [0.2126, 0.7152, 0.0722][i], 0);
return 1.05 / (lum + 0.05);
}

describe('createOverlay', () => {
let overlay: A11yOverlay;

Expand Down Expand Up @@ -173,10 +184,36 @@ describe('createOverlay', () => {
const a = targetEl('a');
overlay.renderTabOrder([stop(a, { order: 1, positive: true, tabindex: 3 })]);
const badge = overlayRoot()!.querySelector('[data-ngb-tab-order]') as HTMLElement;
expect(badge.style.background).toContain('rgb(232, 113, 10)'); // #e8710a warn
expect(badge.style.background).toContain('rgb(180, 83, 9)'); // #b45309 warn
expect(badge.title).toContain('hijacks order');
});

// White 11px label text needs 4.5:1 on its fill for every impact (axe
// flagged the overlay; serious, moderate, minor and none were 2.42 to 4.11).
it.each(['critical', 'serious', 'moderate', 'minor', null] as const)(
'gives a %s finding label at least 4.5:1 contrast with its white text',
(impact) => {
targetEl('target');
overlay.render([finding({ impact })]);
const label = overlayRoot()!.querySelector('[data-impact] span') as HTMLElement;
expect(label.style.color).toBe('rgb(255, 255, 255)');
expect(contrastWithWhite(label.style.background)).toBeGreaterThanOrEqual(4.5);
},
);

// White 10px digits need 4.5:1 on the fill, in light and dark pages alike
// (axe flagged 3.93:1 on the old #0b8f8f teal).
it.each([
['a normal stop', {}],
['a positive-tabindex stop', { positive: true, tabindex: 3 }],
])('gives %s badge at least 4.5:1 contrast with its white digits', (_, extra) => {
const a = targetEl('a');
overlay.renderTabOrder([stop(a, { order: 1, ...extra })]);
const badge = overlayRoot()!.querySelector('[data-ngb-tab-order]') as HTMLElement;
expect(badge.style.color).toBe('rgb(255, 255, 255)');
expect(contrastWithWhite(badge.style.background)).toBeGreaterThanOrEqual(4.5);
});

it('threads a connector polyline through the badge anchor points', () => {
const a = targetEl('a', { top: 100, left: 50, width: 20, height: 20 });
const b = targetEl('b', { top: 200, left: 60, width: 20, height: 20 });
Expand Down
Loading