Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/a11y.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
- run: npm install -g bun
- run: bun install --frozen-lockfile
- run: bun run build
- run: bunx playwright test tests/a11y.spec.ts tests/error-boundary.spec.ts
- run: bunx playwright test tests/a11y.spec.ts tests/error-boundary.spec.ts tests/semantics.spec.ts
- if: failure()
uses: actions/upload-artifact@v4
with:
Expand Down
1 change: 0 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ src/
bun run typecheck # tsc --noEmit
bun run lint # biome check
bun run check # biome check --write (auto-fix lint + formatting)
bun publish --access public # publish to npm
```

### Before pushing: CI must pass
Expand Down
4 changes: 3 additions & 1 deletion app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3476,7 +3476,9 @@ export function App() {
<span className="text-xs text-muted-foreground font-mono w-28 shrink-0">
Title lg
</span>
<Title size="lg">Welcome to Flow</Title>
<Title size="lg" level={2}>
Welcome to Flow
</Title>
</div>
<div className="flex items-baseline gap-6">
<span className="text-xs text-muted-foreground font-mono w-28 shrink-0">
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@flow-industries/ui",
"version": "0.21.2",
"version": "0.22.0",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
35 changes: 27 additions & 8 deletions src/components/ui/time.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function getTimeAgo(date: Date): string {
return `${diffInYears}y`;
}

interface TimeElapsedProps extends React.ComponentProps<"span"> {
interface TimeElapsedProps extends React.ComponentProps<"time"> {
date: Date | string | number;
longFormat?: string;
}
Expand Down Expand Up @@ -59,15 +59,24 @@ function TimeElapsed({
const diffInWeeks =
(Date.now() - dateObj.getTime()) / (1000 * 60 * 60 * 24 * 7);
const display = diffInWeeks > 3 ? format(dateObj, longFormat) : label;
const absoluteLabel = format(dateObj, "PPPPpppp");

return (
<span ref={ref} data-slot="time-elapsed" className={className} {...props}>
{display}
</span>
<time
ref={ref}
data-slot="time-elapsed"
className={className}
dateTime={dateObj.toISOString()}
title={absoluteLabel}
{...props}
>
<span aria-hidden="true">{display}</span>
<span className="sr-only">{absoluteLabel}</span>
</time>
);
}

interface TimeSinceProps extends React.ComponentProps<"span"> {
interface TimeSinceProps extends React.ComponentProps<"time"> {
date: Date | string | number;
format?: string;
}
Expand All @@ -79,10 +88,20 @@ function TimeSince({
ref,
...props
}: TimeSinceProps) {
const dateObj = toDate(date);
const absoluteLabel = format(dateObj, "PPPPpppp");
return (
<span ref={ref} data-slot="time-since" className={className} {...props}>
{format(toDate(date), fmt)}
</span>
<time
ref={ref}
data-slot="time-since"
className={className}
dateTime={dateObj.toISOString()}
title={absoluteLabel}
{...props}
>
<span aria-hidden="true">{format(dateObj, fmt)}</span>
<span className="sr-only">{absoluteLabel}</span>
</time>
);
}

Expand Down
17 changes: 15 additions & 2 deletions src/components/ui/typography.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,25 @@ const titleStyles = {

const titleTags = { sm: "h3", default: "h2", md: "h2", lg: "h1" } as const;

const headingTags = {
1: "h1",
2: "h2",
3: "h3",
4: "h4",
5: "h5",
6: "h6",
} as const;

function Title({
className,
size = "default",
level,
...props
}: React.ComponentProps<"h1"> & { size?: "sm" | "default" | "md" | "lg" }) {
const Tag = titleTags[size];
}: React.ComponentProps<"h1"> & {
size?: "sm" | "default" | "md" | "lg";
level?: 1 | 2 | 3 | 4 | 5 | 6;
}) {
const Tag = level ? headingTags[level] : titleTags[size];
return (
<Tag
data-slot="title"
Expand Down
42 changes: 42 additions & 0 deletions tests/semantics.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { expect, test } from "@playwright/test";

test("heading level overrides size without changing its style", async ({
page,
}) => {
await page.goto("/#design");
const heading = page.getByRole("heading", {
name: "Welcome to Flow",
exact: true,
});
await expect(heading).toHaveJSProperty("tagName", "H2");
const defaultHeading = page.getByRole("heading", {
name: "@flow-industries/ui",
exact: true,
});
await expect(defaultHeading).toHaveJSProperty("tagName", "H1");
const fontSize = await defaultHeading.evaluate(
(element) => getComputedStyle(element).fontSize,
);
await expect(heading).toHaveCSS("font-size", fontSize);
});

test("relative and calendar times expose exact timestamps", async ({
page,
}) => {
await page.goto("/#components");
for (const slot of ["time-elapsed", "time-since"]) {
const times = page.locator(`[data-slot="${slot}"]`);
await expect(times.first()).toBeVisible();
for (const time of await times.all()) {
await expect(time).toHaveJSProperty("tagName", "TIME");
const iso = await time.getAttribute("datetime");
expect(iso).toBeTruthy();
expect(Number.isNaN(Date.parse(iso ?? ""))).toBe(false);
await expect(time.locator(".sr-only")).toHaveText(/\d{4}/);
await expect(time).toHaveAttribute(
"title",
(await time.locator(".sr-only").textContent()) ?? "",
);
}
}
});
Loading