mirror of
https://github.com/block/buzz.git
synced 2026-10-28 21:11:48 +00:00
fix(design-system): show grammar rules in flat tables
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user