Skip to content
Draft
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
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