fix(design-system): show grammar rules in flat tables

Signed-off-by: Arjun Mahanti <arjun@squareup.com>
This commit is contained in:
Arjun Mahanti
2026-09-10 06:00:03 -07:00
parent 37ca6f63c7
commit e06c7e5362
5 changed files with 48 additions and 80 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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;

View File

@@ -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 (
<article className="grammar-page">
<PageHeader title="Logic & grammar" />
@@ -43,59 +29,45 @@ export function GrammarPage() {
<a
key={section.id}
href={`#${section.id}`}
onClick={() => {
const topic = document.getElementById(section.id);
if (topic instanceof HTMLDetailsElement) topic.open = true;
}}
className="text-caption text-accent underline"
>
{section.title}
</a>
))}
</nav>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-10">
{sections.map((section) => (
<details
<section
key={section.id}
id={section.id}
className="grammar-section rounded-control bg-inset p-4"
open={section.id === "role-resolution"}
className="grammar-section"
aria-labelledby={`${section.id}-heading`}
>
<summary className="cursor-pointer text-subheading text-primary">
<h2
id={`${section.id}-heading`}
className="text-heading text-primary"
>
{section.title}
</summary>
<div className="hidden sm:block">
<Table className="grammar-table mt-5">
<caption className="bui-sr-only">{section.title}</caption>
<thead>
<tr>
<th scope="col">Choice</th>
<th scope="col">Use</th>
<th scope="col">Boundary</th>
</h2>
<Table className="grammar-table mt-5 min-w-xl" tabIndex={0}>
<caption className="bui-sr-only">{section.title}</caption>
<thead>
<tr>
<th scope="col">Choice</th>
<th scope="col">Use</th>
<th scope="col">Boundary</th>
</tr>
</thead>
<tbody>
{section.rows.map(([choice, use, boundary]) => (
<tr key={choice}>
<th scope="row">{choice}</th>
<td>{use}</td>
<td>{boundary}</td>
</tr>
</thead>
<tbody>
{section.rows.map(([choice, use, boundary]) => (
<tr key={choice}>
<th scope="row">{choice}</th>
<td>{use}</td>
<td>{boundary}</td>
</tr>
))}
</tbody>
</Table>
</div>
<dl className="sm:hidden mt-4 flex flex-col gap-5">
{section.rows.map(([choice, use, boundary]) => (
<div key={choice} className="flex flex-col gap-2">
<dt className="text-label text-primary break-words">
{choice}
</dt>
<dd className="text-body text-primary">{use}</dd>
<dd className="text-caption text-secondary">{boundary}</dd>
</div>
))}
</dl>
))}
</tbody>
</Table>
<div className="flex flex-wrap gap-x-4 gap-y-2 mt-4">
{section.sources.map((source) => (
<a
@@ -107,7 +79,7 @@ export function GrammarPage() {
</a>
))}
</div>
</details>
</section>
))}
</div>
</article>

View File

@@ -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")