diff --git a/desktop-next/README.md b/desktop-next/README.md index ecd40aaa76..2b7c58953c 100644 --- a/desktop-next/README.md +++ b/desktop-next/README.md @@ -30,9 +30,9 @@ are shared. ## Logic & grammar -`/design/grammar` is a compact BlockUI reference under System. Expand a topic to -read its selection rules, usage boundaries, and links to the verified upstream -snapshot. Topic links support direct loading and refresh. It covers foundations, +`/design/grammar` is a BlockUI reference under System. Every topic is displayed +as a flat table of selection rules and usage boundaries, with links to the +verified upstream snapshot. Tables scroll horizontally on narrow screens. Topic links support direct loading and refresh. It covers foundations, component contracts, applet composition, and generation constraints; the final topic records differences from Buzz's active token contract and upstream gaps. Source links require Block access, but the page has no private runtime dependency. diff --git a/desktop-next/TESTING.md b/desktop-next/TESTING.md index d6cb05f73b..e6db922b66 100644 --- a/desktop-next/TESTING.md +++ b/desktop-next/TESTING.md @@ -56,6 +56,6 @@ portal density, refresh, and appearance preferences. Representative pages are checked at 390/884/1440 in both densities and audited in both themes, with distinct per-component screenshots. -Grammar-page coverage checks sidebar navigation, keyboard disclosure controls, -reopening the same topic link, hash links across reloads, all 19 typography rows, +Grammar-page coverage checks sidebar navigation, always-visible tables, +keyboard topic links, hash links across reloads, all 19 typography rows, source-link destinations, responsive tables, and accessibility in both themes. diff --git a/desktop-next/src/features/design-system/catalog/catalog.css b/desktop-next/src/features/design-system/catalog/catalog.css index 218b903eba..30abf08056 100644 --- a/desktop-next/src/features/design-system/catalog/catalog.css +++ b/desktop-next/src/features/design-system/catalog/catalog.css @@ -57,6 +57,10 @@ .grammar-table { table-layout: fixed; } +.grammar-table:focus-visible { + outline: 2px solid var(--ring-focus); + outline-offset: -2px; +} .grammar-table th, .grammar-table td { white-space: normal; diff --git a/desktop-next/src/features/design-system/ui/GrammarPage.tsx b/desktop-next/src/features/design-system/ui/GrammarPage.tsx index 1bc61629f6..529fccfa66 100644 --- a/desktop-next/src/features/design-system/ui/GrammarPage.tsx +++ b/desktop-next/src/features/design-system/ui/GrammarPage.tsx @@ -1,5 +1,4 @@ import { Table } from "@buzz/ui"; -import { useEffect } from "react"; import { foundationGrammar } from "../grammar/foundations"; import { typographyGrammar } from "../grammar/typography"; import { compositionGrammar } from "../grammar/composition"; @@ -15,19 +14,6 @@ const sections = [ /** Cited BlockUI reference guidance, separate from Buzz's active token contract. */ export function GrammarPage() { - useEffect(() => { - const revealTopic = () => { - const topic = document.getElementById(window.location.hash.slice(1)); - if (topic instanceof HTMLDetailsElement) { - topic.open = true; - topic.scrollIntoView({ block: "start" }); - } - }; - revealTopic(); - window.addEventListener("hashchange", revealTopic); - return () => window.removeEventListener("hashchange", revealTopic); - }, []); - return (
@@ -43,59 +29,45 @@ export function GrammarPage() { { - const topic = document.getElementById(section.id); - if (topic instanceof HTMLDetailsElement) topic.open = true; - }} className="text-caption text-accent underline" > {section.title} ))} -
+
{sections.map((section) => ( -
- +

{section.title} -

-
- - - - - - - + +
{section.title}
ChoiceUseBoundary
+ + + + + + + + + + {section.rows.map(([choice, use, boundary]) => ( + + + + - - - {section.rows.map(([choice, use, boundary]) => ( - - - - - - ))} - -
{section.title}
ChoiceUseBoundary
{choice}{use}{boundary}
{choice}{use}{boundary}
-
-
- {section.rows.map(([choice, use, boundary]) => ( -
-
- {choice} -
-
{use}
-
{boundary}
-
- ))} -
+ ))} + +
{section.sources.map((source) => ( ))}
-
+ ))}
diff --git a/desktop-next/tests/grammar.spec.ts b/desktop-next/tests/grammar.spec.ts index b15af111fb..dfe787e075 100644 --- a/desktop-next/tests/grammar.spec.ts +++ b/desktop-next/tests/grammar.spec.ts @@ -2,9 +2,7 @@ import AxeBuilder from "@axe-core/playwright"; import { expect, test } from "@playwright/test"; import { waitForAnimations } from "../../desktop/tests/helpers/animations"; -test("grammar navigation opens focused topics and preserves deep links", async ({ - page, -}) => { +test("grammar shows all tables and preserves topic links", async ({ page }) => { await page.goto("/design/typography"); await page .getByRole("navigation", { name: "Design system", exact: true }) @@ -13,13 +11,17 @@ test("grammar navigation opens focused topics and preserves deep links", async ( await expect( page.getByRole("heading", { name: "Logic & grammar", exact: true }), ).toBeVisible(); + await expect(page.locator(".grammar-page details")).toHaveCount(0); + await expect(page.locator(".grammar-page table")).toHaveCount(15); + for (const table of await page.locator(".grammar-page table").all()) { + await expect(table).toBeVisible(); + } const topic = page.locator("#type-roles"); const link = page .getByRole("navigation", { name: "Grammar topics" }) .getByRole("link", { name: "The 19 typography roles", exact: true }); await link.focus(); await link.press("Enter"); - await expect(topic).toHaveAttribute("open", ""); await expect(topic.locator("tbody tr")).toHaveCount(19); await expect( topic.getByRole("rowheader", { name: "body/label-medium", exact: true }), @@ -30,22 +32,15 @@ test("grammar navigation opens focused topics and preserves deep links", async ( "href", /github\.com\/squareup\/design-blockinterface\/blob\/[a-f0-9]{40}\/blockUI\/docs\/type\.roles\.md$/, ); - await topic.locator("summary").press("Space"); - await expect(topic).not.toHaveAttribute("open", ""); - // A second click on the same hash must reopen a manually closed topic. - await link.click(); - await expect(topic).toHaveAttribute("open", ""); await page.reload(); - await expect(topic).toHaveAttribute("open", ""); await expect(topic).toBeInViewport(); await page.goto("/design/grammar#buzz-boundary"); - await expect(page.locator("#buzz-boundary")).toHaveAttribute("open", ""); await expect( page.getByRole("rowheader", { name: "Buzz fonts and type", exact: true }), ).toBeVisible(); }); -test("grammar tables fit both densities and themes with accessible disclosures", async ({ +test("grammar tables fit both densities and themes", async ({ page, }, testInfo) => { await page.goto("/design/grammar"); @@ -53,19 +48,16 @@ test("grammar tables fit both densities and themes with accessible disclosures", await page.setViewportSize({ width, height: 960 }); for (const density of ["Normal", "Compact"]) { await page.getByRole("button", { name: density, exact: true }).click(); - for (const section of await page.locator(".grammar-section").all()) { - if ( - !(await section.evaluate((el) => (el as HTMLDetailsElement).open)) - ) { - await section.locator("summary").click(); - } - } expect( await page.evaluate( () => document.documentElement.scrollWidth <= innerWidth, ), ).toBe(true); } + const audit = await new AxeBuilder({ page }) + .disableRules(["color-contrast"]) + .analyze(); + expect(audit.violations).toEqual([]); } for (const mode of ["light", "dark"]) { if (mode === "dark")