@astryxdesign/core 0.7.0 → 0.7.1-canary.7e441be
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BottomSheet/BottomSheet.d.ts +1 -1
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +12 -2
- package/dist/ContextMenu/ContextMenu.d.ts +1 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +22 -8
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +3 -26
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +10 -3
- package/dist/Typeahead/BaseTypeahead.d.ts +1 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +12 -3
- package/dist/hooks/useIsomorphicLayoutEffect.d.ts +2 -2
- package/dist/hooks/useIsomorphicLayoutEffect.d.ts.map +1 -1
- package/dist/utils/focusReturn.d.ts +12 -8
- package/dist/utils/focusReturn.d.ts.map +1 -1
- package/dist/utils/focusReturn.js +44 -3
- package/package.json +4 -4
- package/src/BottomSheet/BottomSheet.test.tsx +114 -0
- package/src/BottomSheet/BottomSheet.tsx +12 -2
- package/src/BottomSheet/__tests__/BottomSheetFocusReturn.a11y.browser.spec.ts +96 -0
- package/src/Chat/__tests__/ChatLayoutScrollButtonSurface.a11y.chromium.spec.ts +5 -6
- package/src/CodeBlock/CodeBlock.spec.md +54 -44
- package/src/ContextMenu/ContextMenu.spec.md +1 -1
- package/src/ContextMenu/ContextMenu.test.tsx +36 -3
- package/src/ContextMenu/ContextMenu.tsx +27 -11
- package/src/ContextMenu/__tests__/ContextMenuCursorAnchor.a11y.browser.spec.ts +153 -0
- package/src/Dialog/Dialog.tsx +2 -30
- package/src/DropdownMenu/DropdownMenu.test.tsx +97 -1
- package/src/DropdownMenu/DropdownMenu.tsx +10 -3
- package/src/DropdownMenu/__tests__/DropdownMenuFocusReturn.a11y.browser.spec.ts +73 -0
- package/src/Markdown/Markdown.spec.md +37 -23
- package/src/PowerSearch/PowerSearch.test.tsx +45 -0
- package/src/Typeahead/BaseTypeahead.test.tsx +99 -1
- package/src/Typeahead/BaseTypeahead.tsx +17 -3
- package/src/utils/focusReturn.test.ts +79 -2
- package/src/utils/focusReturn.ts +48 -3
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file BottomSheetFocusReturn.a11y.browser.spec.ts
|
|
5
|
+
* @input The `Core/DropdownMenu` action-sheet story with a trigger the caller
|
|
6
|
+
* renders, in a built Storybook, and real Chromium/WebKit pointer and
|
|
7
|
+
* keyboard input.
|
|
8
|
+
* @output Browser evidence that the focus a BottomSheet returns when it closes
|
|
9
|
+
* follows the last input (docs/architecture/interaction-modality.md INV1):
|
|
10
|
+
* no ring after a pointer close, the ring after a keyboard close.
|
|
11
|
+
* @position Cross-browser proof; jsdom only proves which focus call the sheet
|
|
12
|
+
* makes. The menu's sheet returns focus through BottomSheet's
|
|
13
|
+
* `finalFocusRef`, and a control the caller renders carries no style of its
|
|
14
|
+
* own that would hide a ring, so the ring shown is the browser's answer.
|
|
15
|
+
* Chromium rings it when a keyboard-opened sheet is closed by a pointer;
|
|
16
|
+
* WebKit also rings it after a tap open and a tap close.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {expect, test, type Locator, type Page} from '@playwright/test';
|
|
20
|
+
import {
|
|
21
|
+
DEFAULT_STORYBOOK_DIR,
|
|
22
|
+
serveStorybook,
|
|
23
|
+
type StaticServer,
|
|
24
|
+
} from '@astryxdesign/a11y-spec/storybook';
|
|
25
|
+
|
|
26
|
+
let storybook: StaticServer;
|
|
27
|
+
test.beforeAll(async () => {
|
|
28
|
+
storybook = await serveStorybook(
|
|
29
|
+
process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
test.afterAll(async () => {
|
|
33
|
+
await storybook?.close();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
async function mount(page: Page): Promise<Locator> {
|
|
37
|
+
await page.goto(
|
|
38
|
+
`${storybook.origin}/iframe.html?id=core-dropdownmenu--action-sheet-custom-trigger&viewMode=story`,
|
|
39
|
+
);
|
|
40
|
+
const trigger = page.getByRole('button', {
|
|
41
|
+
name: 'Project actions',
|
|
42
|
+
exact: true,
|
|
43
|
+
});
|
|
44
|
+
await expect(trigger).toBeVisible();
|
|
45
|
+
return trigger;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function openFromKeyboard(page: Page, trigger: Locator): Promise<void> {
|
|
49
|
+
await trigger.focus();
|
|
50
|
+
await page.keyboard.press('Enter');
|
|
51
|
+
await expect(page.getByRole('dialog')).toBeVisible();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function row(page: Page): Locator {
|
|
55
|
+
return page.getByRole('dialog').getByRole('button', {name: /Edit project/});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function expectReturnedFocus(
|
|
59
|
+
page: Page,
|
|
60
|
+
trigger: Locator,
|
|
61
|
+
ring: boolean,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
await expect(page.getByRole('dialog')).toBeHidden();
|
|
64
|
+
await expect(trigger).toBeFocused();
|
|
65
|
+
expect(
|
|
66
|
+
await trigger.evaluate(element => element.matches(':focus-visible')),
|
|
67
|
+
).toBe(ring);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
test('a keyboard-opened sheet closed by a pointer pick returns focus without a ring', async ({
|
|
71
|
+
page,
|
|
72
|
+
}) => {
|
|
73
|
+
const trigger = await mount(page);
|
|
74
|
+
await openFromKeyboard(page, trigger);
|
|
75
|
+
await row(page).click();
|
|
76
|
+
await expectReturnedFocus(page, trigger, false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('a pointer open and pointer pick return focus without a ring', async ({
|
|
80
|
+
page,
|
|
81
|
+
}) => {
|
|
82
|
+
const trigger = await mount(page);
|
|
83
|
+
await trigger.click();
|
|
84
|
+
await expect(page.getByRole('dialog')).toBeVisible();
|
|
85
|
+
await row(page).click();
|
|
86
|
+
await expectReturnedFocus(page, trigger, false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('a keyboard-opened sheet closed by Escape keeps the ring', async ({
|
|
90
|
+
page,
|
|
91
|
+
}) => {
|
|
92
|
+
const trigger = await mount(page);
|
|
93
|
+
await openFromKeyboard(page, trigger);
|
|
94
|
+
await page.keyboard.press('Escape');
|
|
95
|
+
await expectReturnedFocus(page, trigger, true);
|
|
96
|
+
});
|
|
@@ -15,12 +15,11 @@
|
|
|
15
15
|
* output—not an event wrapper, speculative internal structure, or a node
|
|
16
16
|
* created only for layout plumbing."
|
|
17
17
|
*
|
|
18
|
-
* This is not observable in the unit lane
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* the documented target change the pill?
|
|
18
|
+
* This is not observable in the unit lane: jsdom resolves no cascade, so it
|
|
19
|
+
* cannot say which element paints. An override can land on the element carrying
|
|
20
|
+
* the class while that element is not the one a reader sees. Only a real engine
|
|
21
|
+
* can be asked the question that matters: does styling the documented target
|
|
22
|
+
* change the pill?
|
|
24
23
|
*
|
|
25
24
|
* The pill is identified structurally, as the button's parent element, so every
|
|
26
25
|
* measurement below is independent of where the class currently sits.
|
|
@@ -3,11 +3,11 @@ schema_version: 3
|
|
|
3
3
|
template_version: 7
|
|
4
4
|
kind: component
|
|
5
5
|
id: component:CodeBlock
|
|
6
|
-
authority:
|
|
6
|
+
authority: current
|
|
7
7
|
archive_reason: null
|
|
8
8
|
superseded_by: null
|
|
9
|
-
approved_by:
|
|
10
|
-
approved_at:
|
|
9
|
+
approved_by: cixzhang
|
|
10
|
+
approved_at: 2026-10-12
|
|
11
11
|
owners: [cixzhang]
|
|
12
12
|
review_triggers: [public-api, behavior, layout, theming, accessibility, testing]
|
|
13
13
|
verified_by:
|
|
@@ -29,6 +29,7 @@ architecture:
|
|
|
29
29
|
architecture:public-component-api,
|
|
30
30
|
architecture:component-theming-surface,
|
|
31
31
|
architecture:component-test-sufficiency,
|
|
32
|
+
architecture:theme-tokens,
|
|
32
33
|
]
|
|
33
34
|
contributing: []
|
|
34
35
|
system_specs: [spec:AST-025]
|
|
@@ -38,15 +39,15 @@ system_specs: [spec:AST-025]
|
|
|
38
39
|
|
|
39
40
|
## Contract at a glance
|
|
40
41
|
|
|
41
|
-
| Area | Contract
|
|
42
|
-
| ----------------------- |
|
|
43
|
-
| Public contract | `CodeBlock` and `CodeBlockProps` are exported from `@astryxdesign/core/CodeBlock`; `code` is required, and the remaining released props configure language, header, line presentation, copy, wrapping, sizing, syntax theme, and collapse behavior.
|
|
44
|
-
| Behavior | CodeBlock renders a read-only multi-line code surface, applies syntax highlighting when available, preserves the source string for copy, and exposes optional line, header, copy, theme, and disclosure behavior without editing the content.
|
|
45
|
-
| End-user impact | Readers can inspect and copy code, understand optional line emphasis, and operate long or collapsible examples with keyboard and assistive technology.
|
|
46
|
-
| Builder impact | Builders choose the released display axes without an additional migration or opt-in.
|
|
47
|
-
| Compatibility/readiness | The released API and defaults remain stable. The current implementation still gives every code viewport a keyboard stop even when it does not overflow, contrary to `spec:AST-025/FR12`; that retained implementation gap is not accepted here.
|
|
48
|
-
| Review checks | Reject lost code text, copy of a transformed string, an unnamed copy or disclosure control, stale disclosure state/wiring, dropped root props/ref, a theme target off its painter, or viewport focusability that ignores effective overflow.
|
|
49
|
-
| Governing rules | `architecture:public-component-api/INV1,INV5–INV9`; `architecture:component-theming-surface/INV3–INV7,INV12`; `architecture:component-test-sufficiency/INV1–INV7,INV10–INV11`; `spec:AST-025/FR12–FR13
|
|
42
|
+
| Area | Contract |
|
|
43
|
+
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
44
|
+
| Public contract | `CodeBlock` and `CodeBlockProps` are exported from `@astryxdesign/core/CodeBlock`; `code` is required, and the remaining released props configure language, header, line presentation, copy, wrapping, sizing, syntax theme, and collapse behavior; `isDiff` (FR9) adds the diff layer as a caller choice over any language. |
|
|
45
|
+
| Behavior | CodeBlock renders a read-only multi-line code surface, applies syntax highlighting when available, preserves the source string for copy, and exposes optional line, header, copy, theme, and disclosure behavior without editing the content. |
|
|
46
|
+
| End-user impact | Readers can inspect and copy code, understand optional line emphasis, and operate long or collapsible examples with keyboard and assistive technology. |
|
|
47
|
+
| Builder impact | Builders choose the released display axes without an additional migration or opt-in; the diff layer is one more opt-in axis, and five defaulted syntax-theme slots mean no syntax theme has to change for it. |
|
|
48
|
+
| Compatibility/readiness | The released `CodeBlock` API and defaults remain stable; FR9 (the diff layer) is current and an implementation gap until its implementation ships; Markdown's default path for `diff` fences changes with it (`component:Markdown` FR29). The current implementation still gives every code viewport a keyboard stop even when it does not overflow, contrary to `spec:AST-025/FR12`; that retained implementation gap is not accepted here. |
|
|
49
|
+
| Review checks | Reject lost code text, copy of a transformed string, an unnamed copy or disclosure control, stale disclosure state/wiring, dropped root props/ref, a theme target off its painter, or viewport focusability that ignores effective overflow. |
|
|
50
|
+
| Governing rules | `architecture:public-component-api/INV1,INV5–INV9`; `architecture:component-theming-surface/INV3–INV7,INV12`; `architecture:component-test-sufficiency/INV1–INV7,INV10–INV11`; `spec:AST-025/FR12–FR13`; `architecture:theme-tokens` (syntax vocabulary and the diff slot defaults). |
|
|
50
51
|
|
|
51
52
|
This table is a review projection; the body below is authoritative only after owner approval.
|
|
52
53
|
|
|
@@ -78,33 +79,35 @@ Consumer migration instructions belong in consumer docs and release notes.
|
|
|
78
79
|
|
|
79
80
|
## Public concepts
|
|
80
81
|
|
|
81
|
-
| Concept | Closed values or states
|
|
82
|
-
| ------------------ |
|
|
83
|
-
| code content | caller-supplied string
|
|
84
|
-
| language | built-in identifier, custom-tokenizer identifier, or plaintext/unknown
|
|
85
|
-
| line presentation | numbers off/on; highlights absent or one-indexed line set; wrapped/unwrapped
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
|
|
|
82
|
+
| Concept | Closed values or states | Meaning | Availability | Default | Owner | Stability | Invalid-value behavior |
|
|
83
|
+
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ----------- | ------------------- | --------- | --------------------------------------------------------------------- |
|
|
84
|
+
| code content | caller-supplied string | Supplies the exact read-only source that is displayed and copied. | Every render | required | product callsite | stable | rejected by the public type |
|
|
85
|
+
| language | built-in identifier, custom-tokenizer identifier, or plaintext/unknown | Selects tokenization and the optional visible/accessible language label. | Every render | `plaintext` | component:CodeBlock | stable | unsupported built-ins render without tokens |
|
|
86
|
+
| line presentation | numbers off/on; highlights absent or one-indexed line set; wrapped/unwrapped | Adds navigation and emphasis without changing the copied source string. | Code body | off/absent | component:CodeBlock | stable | out-of-range highlights have no rendered line |
|
|
87
|
+
| diff layer | off; on — each line classified by its first character: `+` added, `-` removed, header (`@@`; a `--- `/`+++ ` pair directly before an `@@`), else context | Tints added and removed rows and quiets header rows from the syntax theme while the rest of every line keeps the base language's syntax paint; the glyph stays in display and copy. | Code body, any `language` | off | product callsite | stable | rejected by the public type (boolean) |
|
|
88
|
+
| header | absent; title; language label; title and language label | Gives the snippet context and houses the copy control when any visible header content exists. | Derived from title and label choices | derived | component:CodeBlock | stable | empty visible content still requires AT naming |
|
|
89
|
+
| copy | hidden; available; transient copied feedback | Copies the exact code string, then invokes `onCopy` only after a successful clipboard write. | Every render when enabled | available | component:CodeBlock | stable | failed clipboard writes do not invoke `onCopy` |
|
|
90
|
+
| syntax rendering | auto; CSS ranges; spans; custom tokenizer; optional per-instance syntax theme | Selects a supported paint path without changing the text or public code semantics. | Every render | `auto` | component:CodeBlock | stable | unsupported range capability falls back to spans |
|
|
91
|
+
| collapse | unavailable; expanded; collapsed | Turns an eligible visible header into a disclosure when the line threshold is met. | Long blocks with a header and collapse enabled | unavailable | component:CodeBlock | stable | missing-header or below-threshold blocks remain expanded and ordinary |
|
|
92
|
+
| container geometry | card/section; small/medium; fit-content/custom width; unbounded/bounded height | Selects presentation and overflow geometry while preserving the read-only code task. | Every render | card/md | component:CodeBlock | stable | values follow their documented CSS/type behavior |
|
|
91
93
|
|
|
92
94
|
## Behavioral and layout contract
|
|
93
95
|
|
|
94
|
-
| ID | Candidate invariant
|
|
95
|
-
| --- |
|
|
96
|
-
| FR1 | CodeBlock MUST display the caller's code string as read-only code. Built-in or custom tokenization MAY change syntax paint but MUST NOT change displayed or copied source text.
|
|
97
|
-
| FR2 | A successful copy MUST write the exact `code` string, invoke `onCopy` once, expose localized copied feedback, and preserve the copy control's discoverable purpose. A failed write MUST NOT report success.
|
|
98
|
-
| FR3 | A title or visible non-plaintext language label MUST create the visible header. Hiding the language label MUST NOT remove copy behavior. Headerless copy remains available when enabled.
|
|
99
|
-
| FR4 | Line numbers, highlighted lines, wrapping, width, container presentation, size, and maximum height MUST affect only their documented presentation axes. Numeric `maxHeight={0}` MUST remain a valid zero-pixel bound rather than being discarded.
|
|
100
|
-
| FR5 | Collapse MUST become available only when enabled, a visible header exists, and the line threshold is met. The control MUST expose expanded state and a resolvable controlled region; collapsed content remains mounted but inert, and losing the control expands the region before hiding the control.
|
|
101
|
-
| FR6 | The root and documented header/title/copy targets MUST remain on their visible painters, reflect the documented visual axes, and continue emitting the approved deprecated aliases until their authorized removal window.
|
|
102
|
-
| FR7 | Supported DOM, data, ARIA, class, style, and `xstyle` inputs MUST compose on the documented root, and the public ref MUST reach that root.
|
|
103
|
-
| FR8 | The code viewport MUST enter sequential keyboard navigation only while at least one applicable axis effectively overflows; losing overflow MUST NOT move current focus. It retains an appropriate role and accessible name while focusable.
|
|
96
|
+
| ID | Candidate invariant | Basis | Draft review state |
|
|
97
|
+
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
|
98
|
+
| FR1 | CodeBlock MUST display the caller's code string as read-only code. Built-in or custom tokenization MAY change syntax paint but MUST NOT change displayed or copied source text. | released types, source, tests, consumer docs; established public interface | verify |
|
|
99
|
+
| FR2 | A successful copy MUST write the exact `code` string, invoke `onCopy` once, expose localized copied feedback, and preserve the copy control's discoverable purpose. A failed write MUST NOT report success. | shipped source/tests; `architecture:component-test-sufficiency/INV2–INV6`; WCAG name/status obligations | verify |
|
|
100
|
+
| FR3 | A title or visible non-plaintext language label MUST create the visible header. Hiding the language label MUST NOT remove copy behavior. Headerless copy remains available when enabled. | shipped source/tests/docs; established public interface | verify |
|
|
101
|
+
| FR4 | Line numbers, highlighted lines, wrapping, width, container presentation, size, and maximum height MUST affect only their documented presentation axes. Numeric `maxHeight={0}` MUST remain a valid zero-pixel bound rather than being discarded. | released public type/docs; `architecture:public-component-api/INV3,INV9`; objective JavaScript nullish semantics | verified by focused regression |
|
|
102
|
+
| FR5 | Collapse MUST become available only when enabled, a visible header exists, and the line threshold is met. The control MUST expose expanded state and a resolvable controlled region; collapsed content remains mounted but inert, and losing the control expands the region before hiding the control. | shipped docs/source/tests; WAI-ARIA disclosure pattern and HTML `inert` semantics | verify |
|
|
103
|
+
| FR6 | The root and documented header/title/copy targets MUST remain on their visible painters, reflect the documented visual axes, and continue emitting the approved deprecated aliases until their authorized removal window. | `architecture:component-theming-surface/INV3–INV7,INV12`; source/docs/tests | verify |
|
|
104
|
+
| FR7 | Supported DOM, data, ARIA, class, style, and `xstyle` inputs MUST compose on the documented root, and the public ref MUST reach that root. | `architecture:public-component-api/INV5,INV6,INV8`; shipped source | verify |
|
|
105
|
+
| FR8 | The code viewport MUST enter sequential keyboard navigation only while at least one applicable axis effectively overflows; losing overflow MUST NOT move current focus. It retains an appropriate role and accessible name while focusable. | `spec:AST-025/FR12–FR13` | settled by current authority; implementation gap |
|
|
106
|
+
| FR9 | With the diff layer on (`isDiff`), CodeBlock MUST classify each line by its first character — `+` added, `-` removed, `@@` header, anything else context — except that a `--- ` line (three minus signs and a space, the unified-diff file header form) directly followed by a `+++ ` line and then an `@@` line is a file header (both lines), wherever it occurs; every other `---`/`+++` line, `---i;` included, is removed or added. CodeBlock MUST set the glyph column aside and tokenize the REST of every line (context, added, and removed alike) with the base `language`'s syntax highlighting. Added, removed, and header lines MUST paint as whole rows — the line-number gutter cell included — from the syntax-theme slots `--color-syntax-added`, `--color-syntax-removed`, and `--color-syntax-diff-header`, whose defaults `architecture:theme-tokens` owns; header rows take the syntax theme's `punctuation` colour as text (`--color-text-secondary` in the default theme) and never change paint; context rows keep ordinary paint. On an added or removed row, the line number and the glyph take the row's marker slot — `--color-syntax-added-marker` / `--color-syntax-removed-marker` — so the margin reads the change in colour; every run of code text, painted by a token or not, keeps its syntax-theme colour. In the Astryx default syntax theme the marker slots default to the theme's tinted-text tokens and the header paint to the muted background (`architecture:theme-tokens` owns the values); a syntax theme that does not set them inherits its own colours — the marker slots and the header text its line-number colour (its `punctuation` slot), the header paint none at all (the header row sits on the theme's own syntax background) — so number, glyph, and header text stay legible on that theme's background in every page mode, and the glyph's shape carries the meaning where its colour does not. The glyph (`+`, `-`, the leading space of a context line, or nothing for a context or header line without one — the marker column is then empty and the whole line is code) MUST render in its own fixed-width marker column — secondary colour on context rows and the row's marker slot on added and removed rows, separated from the code by a token gap (`--spacing-2`, a proportion themes may vary like the rest), excluded from selection for layout yet present in the copied text — so the code column starts at the same inline offset on every line and the content's own indentation is never shifted; the gutter order is line number, marker, code, mirrored in RTL with the code's direction unchanged. The header (language label, copy, disclosure) and the viewport's name stay as for any other code: with the layer over plaintext the visible language label and the viewport name are `diff`, and over a language they name both (`tsx diff`). Markdown's fence mapping onto the layer is `component:Markdown` FR29. Row paint MUST compose with line numbers without moving text; on a highlighted added, removed, or header line the highlight REPLACES the row paint rather than stacking on it, so text keeps its single-paint contrast; and row paint MUST span the full line width — the scrolled overflow of a long line included, in wrapped and unwrapped mode — not only the visible viewport. Contrast floor: in the Astryx default syntax theme, every default token colour and the row's marker slot (line number and glyph) on a painted row MUST measure ≥ 4.5:1 against the default tints composited over the code block's own `--color-syntax-background` on the default card surface. In a syntax theme that overrides a diff slot, a token that already measures under 4.5:1 on that theme's own syntax background MUST NOT drop further on the slot, and every other token MUST clear 4.5:1 on it. Two known shortfalls, tracked separately: the shipped syntax presets do not yet meet the floor on the default tints with the layer on, and shipped page themes whose own text tokens fall under it on the tints keep that shortfall, tracked for a fix in those themes, until each sets its tokens or the diff slots. | this record DEC-1 | current; owner-approved 2026-10-12; implementation gap |
|
|
104
107
|
|
|
105
108
|
### Allowed variation
|
|
106
109
|
|
|
107
|
-
- **AV1 — Syntax paint.** Built-in token colors, custom token types, CSS Highlight API use, span fallback, and syntax themes may vary paint without changing the source string or code task.
|
|
110
|
+
- **AV1 — Syntax paint.** Built-in token colors, custom token types, CSS Highlight API use, span fallback, and syntax themes may vary paint without changing the source string or code task. Diff row tints are three syntax-theme slots like any other syntax paint: a syntax theme may harmonize them with its palette, and a theme that sets none keeps the defaults.
|
|
108
111
|
- **AV2 — Theme paint.** Current CodeBlock targets may vary their accepted visual properties without changing text, control semantics, or ownership.
|
|
109
112
|
- **AV3 — Code content.** The product supplies arbitrary code text and the matching language identifier; CodeBlock does not interpret the program's meaning.
|
|
110
113
|
|
|
@@ -174,20 +177,27 @@ Exact CodeBlock proportions and syntax colors are not contract requirements; the
|
|
|
174
177
|
|
|
175
178
|
## Verification map
|
|
176
179
|
|
|
177
|
-
| Contract | Verification
|
|
178
|
-
| ----------------- |
|
|
179
|
-
| FR1, PR1–PR3 | `tokenizer.test.ts`; `highlightRanges.test.ts`; CodeBlock focused tests and built Storybook
|
|
180
|
-
| FR2, AR1, AR3 | `CodeBlock.test.tsx`; component-scoped accessibility audit
|
|
181
|
-
| FR3–FR5, AR2 | `CodeBlock.test.tsx`; `CodeBlock.stories.tsx`; exact-head browser evidence
|
|
182
|
-
| FR6 | `themingTargets.test.ts`; consumer docs; source review
|
|
183
|
-
| FR7 | focused root-prop/ref tests; strict lint/typecheck; consumer-doc validation
|
|
184
|
-
| FR8, AR4 | `spec:AST-025` integration evidence plus real-browser fit/overflow/focus transitions
|
|
185
|
-
|
|
|
186
|
-
|
|
|
180
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
181
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
|
|
182
|
+
| FR1, PR1–PR3 | `tokenizer.test.ts`; `highlightRanges.test.ts`; CodeBlock focused tests and built Storybook | short/long, built-in/custom, ranges/spans, empty lines | Text changes, stale tokens paint, ranges leak, source order changes, or long work stops yielding/canceling. | `audit:CodeBlock/behavior` |
|
|
183
|
+
| FR2, AR1, AR3 | `CodeBlock.test.tsx`; component-scoped accessibility audit | copy success, rapid repeat, localized feedback, empty title | Exact source is not copied, callback fires on failure, announcement is absent/duplicated, or a control is unnamed. | `audit:CodeBlock/a11y` |
|
|
184
|
+
| FR3–FR5, AR2 | `CodeBlock.test.tsx`; `CodeBlock.stories.tsx`; exact-head browser evidence | headerless, titled, zero max height, line options, collapse/control removal | Header/copy placement changes, zero is dropped, disclosure state/wiring/keyboard behavior breaks, or inert outlives its control. | `audit:CodeBlock/behavior` |
|
|
185
|
+
| FR6 | `themingTargets.test.ts`; consumer docs; source review | root, header, title, copy; current and deprecated names | A target leaves its painter, an axis stops reflecting, or a deprecated alias disappears before authorization. | `audit:CodeBlock/theming` |
|
|
186
|
+
| FR7 | focused root-prop/ref tests; strict lint/typecheck; consumer-doc validation | ref plus supported root/style/data/ARIA inputs | The ref misses the root, accepted props disappear, or styling inputs replace rather than compose. | `audit:CodeBlock/api` |
|
|
187
|
+
| FR8, AR4 | `spec:AST-025` integration evidence plus real-browser fit/overflow/focus transitions | fitting, inline overflow, block overflow, losing overflow | A fitting viewport adds a stop, overflow is unreachable, naming disappears, or focus moves when overflow ends. | `audit:CodeBlock/a11y` |
|
|
188
|
+
| FR9 | `tokenizer.test.ts` (diff line classes; file-header pair rule; base-language tokens past the glyph); `CodeBlock.test.tsx` (row attributes, marker column, copy string incl. glyphs, `highlightLines`, header kept with the `diff` / `tsx diff` label and viewport name); syntax-theme tests (five slot defaults and overrides; a preset without marker or header slots takes punctuation text and no header paint); exact-head browser evidence incl. every default syntax token colour and the row marker slot (line number and glyph) measured ≥ 4.5:1 on painted rows in light and dark against the code block's own `--color-syntax-background` on the default card surface (the two known shortfalls in FR9 excepted) (on the light page background the surface is translucent and a plain row's default keyword already sits under the floor — a pre-existing shortfall that affects every CodeBlock there, tracked separately), and equal code-column x-offsets for context, added, and removed lines | added/removed/header/context over `tsx` and over plaintext; a removed line starting with `--`; `---i;` / `+++i;` right before an `@@` (removed/added, not header); a `--- `/`+++ ` pair with no `@@` after it (removed/added); a `--- `/`+++ `/`@@` file header after an earlier hunk (header); the layer over plaintext (label and viewport name `diff`) and over `tsx` (`tsx diff`); a context line with no leading space; a line longer than the viewport, scrolled (tint to the full scroll width) and wrapped; with `highlightLines` on an added line and on a header line (highlight paint only, text measured on it); with line numbers (gutter cell tinted, number in the row's marker slot); plain and token-painted text on a changed row keeping their syntax colours; light and dark (contrast measured on each); a custom syntax theme overriding the slots; indented code under all three line kinds; RTL (gutter mirrored, code direction unchanged) | A glyph removed from display or copy, a changed line's code starting at a different offset than its neighbours, a header row painted as a change or hidden, a header's text or paint taken from the page theme under a defined syntax theme, a `--`-prefixed removed line painted as a header, syntax paint lost past the glyph, a tint from anywhere but the three row slots, a gutter cell left untinted, a line number or glyph left in the ordinary colour on a changed row, code text recoloured by the row, a tint that stops at the viewport edge of a scrolled line, the header, collapse control, or viewport name lost on a diff, a `---i;` line painted as a header, or a default token or marker on a painted row under 4.5:1 on the default surface, or an override on which a token that clears 4.5:1 on the theme's own background falls under it or an already-short token drops further | `audit:CodeBlock/a11y` |
|
|
189
|
+
| RTL | `rtl:audit -- --filter CodeBlock` D2 target plus exact-head browser evidence | title and copy order in LTR/RTL | Logical order fails to mirror or physical placement leaks. | `audit:CodeBlock/rtl` |
|
|
190
|
+
| Consumer metadata | CLI `component CodeBlock --json`; docs typecheck; prop/target validation; package-export checks | released props/defaults/ref and current/deprecated targets | A released prop/default/ref/target disappears from consumer discovery or docs name an unsupported value. | `audit:CodeBlock/docs` |
|
|
187
191
|
|
|
188
192
|
## Decision log
|
|
189
193
|
|
|
190
|
-
|
|
194
|
+
### DEC-1 — Diff is a line layer over any language, tinted by three defaulted syntax-theme slots
|
|
195
|
+
|
|
196
|
+
**Decider:** `cixzhang`, `2026-10-12`
|
|
197
|
+
|
|
198
|
+
A reader of a changelog, migration note, or review comment sees what was added and what was removed at a glance, and still reads the code as code: with the diff layer on, the leading `+`/`-` column is set aside and the rest of every line keeps the base language's syntax highlighting, so a `tsx` diff looks like `tsx` with two kinds of tinted rows. The tints are syntax paint, so they live in the syntax theme as `--color-syntax-added`, `--color-syntax-removed`, and `--color-syntax-diff-header`, with the margin colours beside them as `--color-syntax-added-marker` and `--color-syntax-removed-marker`, all with defaults `architecture:theme-tokens` owns; the gutter cell, line number, and glyph share the row's hue because a reader scanning the margin should find the change there, while the code — every token and every plain run — keeps the syntax theme's colours so a property or identifier reads the same on a changed row as on a context row; a theme may override them to sit with its own palette. No single default strength keeps every shipped preset's passing tokens above the floor on the tints while the tint stays visible — a dark preset under a light page theme receives a pale tint — so the floor binds the default theme and overriding themes, and the presets' shortfall is known and tracked separately. Hunk and file headers stay visible as quiet rows in the syntax theme's own secondary (punctuation) colour — on the muted band in the default theme, on the theme's own background in a defined theme — the treatment the common diff renderers give them — because a fence is the author's exact text: paint never changes it (FR1) and copy round-trips it; an author who wants no headers leaves them out of the fence. A `--- ` line (with its space) is a file header only directly before a `+++ ` line and an `@@` line, so a removed line that starts with `--` — `---i;` right before the next hunk included — is not misread, and a multi-file diff keeps every file header; the one shape the rule cannot tell apart is a removed `-- ` line directly followed by an added `++ ` line and a hunk header, which reads as a file header and is accepted. A Markdown fence selects the layer by its info string on the default path only, so a custom renderer's contract stays unchanged. The `+`/`-` glyph sits in its own marker column with a token gap before the code, so a reader's eye finds the change at the margin and the code's indentation reads exactly as written. On a tinted row the glyph and the line number take the row's marker slot, because the glyph is the cue a reader without colour depends on and it reads with the code it marks; in the default theme the marker slots default to the theme's own tinted-text tokens, which are made to sit on tinted surfaces, and a syntax theme that does not set them keeps its own colours — its line-number colour for the markers and the header text, and no header paint — rather than borrowing the page theme's tokens, which could sit on the wrong polarity under a dark preset on a light page. Rejected: deriving the marker colours from the theme's `string` and `tag` tokens (a theme whose `tag` is green would mark removed rows green); recolouring the code itself to the row hue, all of it or only the runs no token paints (it changes what a property or identifier looks like between rows). A light tint of plain code toward the row hue is not adopted. Text keeps its syntax colours — the semantic text colours fall under the floor on their tints — and the glyphs stay in the text so the change reads without colour and copies exactly. The Markdown fence mapping is Markdown's own fact (its FR29); this record owns only the layer.
|
|
199
|
+
|
|
200
|
+
A per-line typed wash (an added/removed flavour of `highlightLines`) is not decided here: the layer answers the need with the diff text itself. On a highlighted changed line the highlight replaces the tint because two translucent paints would compound into a colour no theme chose and push tokens under the floor. Rejected: hiding header lines (paint would change the displayed text and break copy; authors omit headers by omitting them from the fence); `diff` as a standalone language (every line falls to plaintext, so the code stops reading as code); tinting from the semantic tokens alone with no syntax slots (a custom syntax theme could not harmonize the rows with its palette); colouring the changed text with `--color-success`/`--color-error` (below the 4.5:1 text minimum on the tinted rows); per-language `diff-<lang>` identifiers in the component API (they multiply the language list; the fence convention maps to the prop instead); a separate Diff component (a fence is a CodeBlock and Markdown already routes fences there).
|
|
191
201
|
|
|
192
202
|
## Open questions
|
|
193
203
|
|
|
@@ -146,7 +146,7 @@ the highlight is DOM focus per `module:DropdownMenu/useMenuPress` AR1.
|
|
|
146
146
|
| Touch action row | Presents one data-driven touch action or drill-in entry using ListItem. | `component:List` | Prominent | FR1, FR3, FR4 |
|
|
147
147
|
|
|
148
148
|
The `context-menu` target intentionally appears on both alternative painted
|
|
149
|
-
menu surfaces. Trigger area content stays caller-owned and the
|
|
149
|
+
menu surfaces. Trigger area content stays caller-owned and the 1px cursor
|
|
150
150
|
anchor is positioning infrastructure rather than consumer anatomy.
|
|
151
151
|
|
|
152
152
|
### Theming anatomy
|
|
@@ -658,7 +658,7 @@ describe('ContextMenu compound mode', () => {
|
|
|
658
658
|
});
|
|
659
659
|
|
|
660
660
|
describe('cursor anchor positioning (#3465)', () => {
|
|
661
|
-
// The menu is anchored to a
|
|
661
|
+
// The menu is anchored to a 1px element placed at the cursor point
|
|
662
662
|
// *inside the trigger*, so it is positioned relative to the trigger's
|
|
663
663
|
// context (scroll-follow + auto-flip) rather than the viewport.
|
|
664
664
|
function getCursorAnchor(trigger: HTMLElement): HTMLElement {
|
|
@@ -669,7 +669,7 @@ describe('ContextMenu compound mode', () => {
|
|
|
669
669
|
return anchor;
|
|
670
670
|
}
|
|
671
671
|
|
|
672
|
-
it('renders a
|
|
672
|
+
it('renders a 1px cursor anchor inside the trigger', () => {
|
|
673
673
|
render(
|
|
674
674
|
<ContextMenu items={[{label: 'Item 1'}]} data-testid="ctx">
|
|
675
675
|
<div>Right-click me</div>
|
|
@@ -680,6 +680,9 @@ describe('ContextMenu compound mode', () => {
|
|
|
680
680
|
// Carries an anchor-name so the menu can be anchored to it via CSS
|
|
681
681
|
// anchor positioning (context mode), not fixed viewport coordinates.
|
|
682
682
|
expect(anchor.style.anchorName).toMatch(/^--astryx-layer-/);
|
|
683
|
+
// Owns a box: WebKit counts an empty anchor box inside a scroll
|
|
684
|
+
// container as clipped, and `position-visibility` hides the menu.
|
|
685
|
+
expect(anchor).toHaveStyle({width: '1px', height: '1px'});
|
|
683
686
|
});
|
|
684
687
|
|
|
685
688
|
it('places the cursor anchor at the pointer position relative to the trigger', () => {
|
|
@@ -725,7 +728,37 @@ describe('ContextMenu compound mode', () => {
|
|
|
725
728
|
fireEvent.contextMenu(trigger, {clientX: 0, clientY: 0, detail: 0});
|
|
726
729
|
const anchor = getCursorAnchor(trigger);
|
|
727
730
|
expect(anchor.style.left).toBe('0px');
|
|
728
|
-
|
|
731
|
+
// The 1px anchor steps back into the trigger's last pixel row, so its
|
|
732
|
+
// bottom edge (where the menu starts) is still the trigger's bottom.
|
|
733
|
+
expect(anchor.style.top).toBe('19px');
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it('keeps the anchor inside the trigger at its end edges, and nowhere else', () => {
|
|
737
|
+
render(
|
|
738
|
+
<ContextMenu items={[{label: 'Item 1'}]} data-testid="ctx">
|
|
739
|
+
<div>Right-click me</div>
|
|
740
|
+
</ContextMenu>,
|
|
741
|
+
);
|
|
742
|
+
const trigger = screen.getByTestId('ctx');
|
|
743
|
+
trigger.getBoundingClientRect = () =>
|
|
744
|
+
({
|
|
745
|
+
left: 100,
|
|
746
|
+
top: 50,
|
|
747
|
+
right: 300,
|
|
748
|
+
bottom: 150,
|
|
749
|
+
width: 200,
|
|
750
|
+
height: 100,
|
|
751
|
+
}) as DOMRect;
|
|
752
|
+
const anchor = getCursorAnchor(trigger);
|
|
753
|
+
// A point on the trigger's end edges steps back by the anchor's pixel.
|
|
754
|
+
fireEvent.contextMenu(trigger, {clientX: 300, clientY: 150, detail: 1});
|
|
755
|
+
expect(anchor.style.left).toBe('199px');
|
|
756
|
+
expect(anchor.style.top).toBe('99px');
|
|
757
|
+
// A point past the trigger (content overflowing it) stays at the
|
|
758
|
+
// pointer, as does a point on the trigger's start edges.
|
|
759
|
+
fireEvent.contextMenu(trigger, {clientX: 320, clientY: 50, detail: 1});
|
|
760
|
+
expect(anchor.style.left).toBe('220px');
|
|
761
|
+
expect(anchor.style.top).toBe('0px');
|
|
729
762
|
});
|
|
730
763
|
});
|
|
731
764
|
});
|