Skip to content
Draft
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d89bd41
test(accessibility): define forced-colors state contract
seonghobae Aug 10, 2026
d94c6b5
fix(accessibility): preserve forced-colors state cues
seonghobae Aug 10, 2026
1b34fe4
test(accessibility): verify forced-colors in real engines
seonghobae Aug 10, 2026
3fdd42d
test(accessibility): define editor focus-visible contract
seonghobae Aug 10, 2026
3b44e67
fix(accessibility): restore editor focus-visible cue
seonghobae Aug 10, 2026
23a24f5
test(accessibility): prove editable forced-colors focus cue in browser
seonghobae Aug 10, 2026
bcd993a
test(a11y): prove forced-colors cascade ordering
seonghobae Aug 10, 2026
bd10925
fix(a11y): place forced-colors overrides after base rules
seonghobae Aug 10, 2026
940c2e1
test(a11y): scope forced-colors order before print media
seonghobae Aug 10, 2026
862db1d
test(a11y): distinguish media overrides from later screen rules
seonghobae Aug 10, 2026
a517183
chore(a11y): synchronize forced-colors lane with protected main
seonghobae Aug 17, 2026
5a0d410
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 26, 2026
f522d1f
fix(a11y): unify forced-colors layer into one cascade-effective block
seonghobae Aug 26, 2026
48fffe1
test(accessibility): drive forced-colors focus by keyboard
seonghobae Aug 26, 2026
a10b7c9
test(accessibility): run forced-colors browser coverage
seonghobae Aug 28, 2026
eb3618c
fix(accessibility): respect browser harness ownership
seonghobae Aug 28, 2026
9cf8192
Merge remote-tracking branch 'origin/test/cjk-input-assurance-376' in…
seonghobae Sep 4, 2026
3378ac4
test(accessibility): use keyboard forced-colors journey
seonghobae Sep 4, 2026
e892384
test(accessibility): follow macOS WebKit tab semantics
seonghobae Sep 4, 2026
74580c1
chore(accessibility): synchronize input parent
seonghobae Sep 4, 2026
a7d9919
chore: synchronize input assurance parent
seonghobae Sep 5, 2026
9f82913
merge: inherit current input assurance into forced colors owner
seonghobae Sep 5, 2026
4bc3c8f
test(a11y): detect clipped controls in the real narrow toolbar
seonghobae Sep 6, 2026
e53eefa
fix(a11y): wrap toolbar groups before controls are clipped
seonghobae Sep 6, 2026
dd1a780
merge: inherit the verified input-owner teardown regression
seonghobae Sep 6, 2026
60fa76e
test: retain responsive keyboard focus screenshots
seonghobae Sep 6, 2026
8f8ff6e
test: expose disabled colors on readable editor content
seonghobae Sep 6, 2026
f97452a
Merge canonical browser stylesheet provenance into UI assurance
seonghobae Sep 6, 2026
a069699
fix: preserve readable content colors in forced color mode
seonghobae Sep 6, 2026
0e8d8e3
test: match the real toolbar focus label
seonghobae Sep 6, 2026
8378380
test: inspect real pressed toolbar state and empty guidance
seonghobae Sep 6, 2026
a9f2775
test: distinguish active-button transition and settled paint
seonghobae Sep 6, 2026
f6dbf52
test: expose forced-color highlight paint failures
seonghobae Sep 6, 2026
7f6edc7
fix: avoid interpolated colors in forced-color buttons
seonghobae Sep 6, 2026
0c29f3f
test: exercise authored collaboration colors in forced mode
seonghobae Sep 6, 2026
7a95e5a
fix: preserve readable system highlight labels
seonghobae Sep 6, 2026
85e6444
test: record forced-color adjustment capability per engine
seonghobae Sep 6, 2026
cc766fd
merge: inherit canonical collaboration browser probe
seonghobae Sep 6, 2026
59d3220
merge: inherit canonical browser archive evidence guard
seonghobae Sep 6, 2026
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
23 changes: 23 additions & 0 deletions src/editorFocusStyles.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';

import { describe, expect, it } from 'vitest';

const styles = readFileSync(resolve(process.cwd(), 'src/styles.css'), 'utf8');
const forcedColorsIndex = styles.indexOf('@media (forced-colors: active)');
const forcedColorsStyles =
forcedColorsIndex >= 0 ? styles.slice(forcedColorsIndex) : '';

describe('editable surface focus stylesheet contract', () => {
it('replaces the removed browser outline with a visible keyboard focus cue', () => {
expect(styles).toMatch(
/\.cwl-editor__content:focus-visible\s*\{[^}]*outline:\s*2px\s+solid\s+var\(--cwl-accent\)\s*;[^}]*outline-offset:\s*-2px\s*;/u,
);
});

it('keeps the editable focus cue visible in forced-colors mode', () => {
expect(forcedColorsStyles).toMatch(
/\.cwl-editor__content:focus-visible\s*\{[^}]*outline:\s*2px\s+solid\s+Highlight\s*;[^}]*outline-offset:\s*-2px\s*;/u,
);
});
});
109 changes: 109 additions & 0 deletions src/forcedColorsStyles.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';

import { describe, expect, it } from 'vitest';

const styles = readFileSync(resolve(process.cwd(), 'src/styles.css'), 'utf8');
const printIndex = styles.indexOf('@media print');
const screenStyles = printIndex >= 0 ? styles.slice(0, printIndex) : styles;
const forcedColorsIndex = screenStyles.indexOf('@media (forced-colors: active)');

const findCssBlockEnd = (source: string, startIndex: number): number => {
const openingBrace = source.indexOf('{', startIndex);
if (openingBrace < 0) return -1;

let depth = 0;
for (let index = openingBrace; index < source.length; index += 1) {
if (source[index] === '{') depth += 1;
if (source[index] !== '}') continue;
depth -= 1;
if (depth === 0) return index + 1;
}
return -1;
};

const forcedColorsEnd =
forcedColorsIndex >= 0 ? findCssBlockEnd(screenStyles, forcedColorsIndex) : -1;
const forcedColorsStyles =
forcedColorsEnd > forcedColorsIndex
? screenStyles.slice(forcedColorsIndex, forcedColorsEnd)
: '';
const beforeForcedColorsStyles =
forcedColorsIndex >= 0 ? screenStyles.slice(0, forcedColorsIndex) : screenStyles;
const afterForcedColorsStyles =
forcedColorsEnd >= 0 ? screenStyles.slice(forcedColorsEnd) : screenStyles;
const forcedColorsBlocks =
screenStyles.match(/@media \(forced-colors: active\)/gu) ?? [];
const baseStateSelectors = [
'.cwl-editor__content a {',
'.cwl-editor__content blockquote {',
'.cwl-collaboration-status {',
'.collaboration-cursor__label {',
];

describe('forced-colors stylesheet contract', () => {
it('defines one final screen forced-colors override layer after base state rules', () => {
expect(printIndex).toBeGreaterThan(-1);
expect(forcedColorsIndex).toBeGreaterThan(-1);
expect(forcedColorsEnd).toBeGreaterThan(forcedColorsIndex);
expect(forcedColorsBlocks).toHaveLength(1);
for (const selector of baseStateSelectors) {
expect(beforeForcedColorsStyles).toContain(selector);
expect(afterForcedColorsStyles).not.toContain(selector);
}
});

it('defines a forced-colors boundary using system colors', () => {
expect(forcedColorsIndex).toBeGreaterThan(-1);
expect(forcedColorsStyles).toContain('Canvas');
expect(forcedColorsStyles).toContain('CanvasText');
expect(forcedColorsStyles).toContain('ButtonFace');
expect(forcedColorsStyles).toContain('ButtonText');
expect(forcedColorsStyles).toContain('Highlight');
expect(forcedColorsStyles).toContain('HighlightText');
expect(forcedColorsStyles).toContain('GrayText');
expect(forcedColorsStyles).toContain('LinkText');
});

it('keeps keyboard focus and active toolbar state visible without theme colors', () => {
expect(forcedColorsStyles).toMatch(
/\.cwl-tb-btn:focus-visible\s*\{[^}]*outline:\s*2px\s+solid\s+Highlight\s*;[^}]*outline-offset:\s*2px\s*;/u,
);
expect(forcedColorsStyles).toMatch(
/\.cwl-tb-btn\.is-active\s*\{[^}]*background:\s*Highlight\s*;[^}]*border-color:\s*Highlight\s*;[^}]*color:\s*HighlightText\s*;/u,
);
});

it('does not use opacity as the only disabled-state cue', () => {
expect(forcedColorsStyles).toMatch(
/\.cwl-tb-btn:disabled\s*\{[^}]*opacity:\s*1\s*;[^}]*color:\s*GrayText\s*;[^}]*border-color:\s*GrayText\s*;/u,
);
});

it('preserves high-contrast chrome, document links, and collaboration cues', () => {
expect(forcedColorsStyles).toMatch(
/\.cwl-editor\s*\{[^}]*color:\s*CanvasText\s*;[^}]*background:\s*Canvas\s*;[^}]*border-color:\s*CanvasText\s*;/u,
);
expect(forcedColorsStyles).toMatch(
/\.cwl-toolbar[\s\S]*\.cwl-collaboration-status[\s\S]*\{[^}]*background:\s*ButtonFace\s*;[^}]*color:\s*ButtonText\s*;/u,
);
expect(forcedColorsStyles).toMatch(
/\.cwl-editor__content a\s*\{[^}]*color:\s*LinkText\s*;/u,
);
expect(forcedColorsStyles).toMatch(
/\.collaboration-cursor__caret\s*\{[^}]*border-left-color:\s*Highlight\s*;/u,
);
expect(forcedColorsStyles).toMatch(
/\.collaboration-cursor__label\s*\{[^}]*background:\s*Highlight\s*;[^}]*color:\s*HighlightText\s*;/u,
);
});

it('preserves authored structural boundaries in forced colors', () => {
expect(forcedColorsStyles).toMatch(
/\.cwl-tb-group\s*\{[^}]*border-right-color:\s*CanvasText\s*;/u,
);
expect(forcedColorsStyles).toMatch(
/\.cwl-editor__content code,[\s\S]*\.cwl-editor__content pre,[\s\S]*\.cwl-editor__content th,[\s\S]*\.cwl-editor__content td\s*\{[^}]*border-color:\s*CanvasText\s*;/u,
);
});
});
99 changes: 93 additions & 6 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,6 @@
cursor: not-allowed;
}

@media (forced-colors: active) {
.cwl-tb-btn:focus-visible {
outline-color: CanvasText;
}
}

.cwl-editor__surface {
overflow-y: auto;
max-height: 70vh;
Expand All @@ -129,6 +123,11 @@
outline: none;
}

.cwl-editor__content:focus-visible {
outline: 2px solid var(--cwl-accent);
outline-offset: -2px;
}

.cwl-editor__content > * + * {
margin-top: 0.75em;
}
Expand Down Expand Up @@ -260,6 +259,94 @@
padding-top: calc(16px + 1.6em);
}

@media (forced-colors: active) {
.cwl-editor {
color: CanvasText;
background: Canvas;
border-color: CanvasText;
}

.cwl-toolbar,
.cwl-collaboration-status {
background: ButtonFace;
color: ButtonText;
border-bottom-color: CanvasText;
}

.cwl-tb-group {
border-right-color: CanvasText;
}

.cwl-tb-btn {
background: ButtonFace;
color: ButtonText;
border-color: ButtonText;
}

.cwl-tb-btn:hover:not(:disabled) {
background: Highlight;
color: HighlightText;
}

.cwl-tb-btn:focus-visible {
outline: 2px solid Highlight;
outline-offset: 2px;
}

.cwl-tb-btn.is-active {
background: Highlight;
border-color: Highlight;
color: HighlightText;
}

.cwl-tb-btn:disabled {
opacity: 1;
color: GrayText;
border-color: GrayText;
}

.cwl-editor__content:focus-visible {
outline: 2px solid Highlight;
outline-offset: -2px;
}

.cwl-editor__content a {
color: LinkText;
}

.cwl-editor__content blockquote {
border-left-color: CanvasText;
color: GrayText;
}

.cwl-editor__content .is-editor-empty:first-child::before {
color: GrayText;
}

.cwl-editor__content code,
.cwl-editor__content pre,
.cwl-editor__content th,
.cwl-editor__content td {
border-color: CanvasText;
}

.cwl-editor__content code,
.cwl-editor__content pre,
.cwl-editor__content th {
background: Canvas;
color: CanvasText;
}

.collaboration-cursor__caret {
border-left-color: Highlight;
}

.collaboration-cursor__label {
background: Highlight;
color: HighlightText;
}
}

@media print {
.cwl-editor {
--cwl-fg: #000000;
Expand Down
Loading
Loading