fix: address Storybook review findings
This commit is contained in:
parent
d95f064189
commit
17c9d24e31
8 changed files with 121 additions and 20 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
import type { StorybookConfig } from "@storybook/sveltekit";
|
import type { StorybookConfig } from "@storybook/sveltekit";
|
||||||
|
|
||||||
const config: StorybookConfig = {
|
const config: StorybookConfig = {
|
||||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|ts|svelte)"],
|
stories: ["../src/**/*.stories.@(js|ts|svelte)"],
|
||||||
addons: [
|
addons: [
|
||||||
"@storybook/addon-svelte-csf",
|
"@storybook/addon-svelte-csf",
|
||||||
"@storybook/addon-a11y",
|
"@storybook/addon-a11y",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,16 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type { HTMLButtonAttributes } from "svelte/elements";
|
||||||
import IconGlyph from "./IconGlyph.svelte";
|
import IconGlyph from "./IconGlyph.svelte";
|
||||||
|
|
||||||
|
type ButtonProps = Omit<HTMLButtonAttributes, "type"> & {
|
||||||
|
label: string;
|
||||||
|
variant?: "primary" | "secondary" | "danger" | "ghost";
|
||||||
|
size?: "default" | "compact";
|
||||||
|
icon?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
type?: "button" | "submit" | "reset";
|
||||||
|
};
|
||||||
|
|
||||||
let {
|
let {
|
||||||
label,
|
label,
|
||||||
variant = "primary",
|
variant = "primary",
|
||||||
|
|
@ -9,19 +19,14 @@
|
||||||
disabled = false,
|
disabled = false,
|
||||||
loading = false,
|
loading = false,
|
||||||
type = "button",
|
type = "button",
|
||||||
}: {
|
class: className = "",
|
||||||
label: string;
|
...buttonProps
|
||||||
variant?: "primary" | "secondary" | "danger" | "ghost";
|
}: ButtonProps = $props();
|
||||||
size?: "default" | "compact";
|
|
||||||
icon?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
loading?: boolean;
|
|
||||||
type?: "button" | "submit" | "reset";
|
|
||||||
} = $props();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="ui-button"
|
{...buttonProps}
|
||||||
|
class={className ? `ui-button ${className}` : "ui-button"}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
data-size={size}
|
data-size={size}
|
||||||
data-loading={loading}
|
data-loading={loading}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,33 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type { HTMLButtonAttributes } from "svelte/elements";
|
||||||
import IconGlyph from "./IconGlyph.svelte";
|
import IconGlyph from "./IconGlyph.svelte";
|
||||||
|
|
||||||
|
type IconButtonProps = Omit<HTMLButtonAttributes, "type"> & {
|
||||||
|
icon: string;
|
||||||
|
label: string;
|
||||||
|
active?: boolean;
|
||||||
|
type?: "button" | "submit" | "reset";
|
||||||
|
};
|
||||||
|
|
||||||
let {
|
let {
|
||||||
icon,
|
icon,
|
||||||
label,
|
label,
|
||||||
active = false,
|
active = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}: {
|
type = "button",
|
||||||
icon: string;
|
class: className = "",
|
||||||
label: string;
|
...buttonProps
|
||||||
active?: boolean;
|
}: IconButtonProps = $props();
|
||||||
disabled?: boolean;
|
|
||||||
} = $props();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button class="icon-button" aria-label={label} data-active={active} {disabled}>
|
<button
|
||||||
|
{...buttonProps}
|
||||||
|
class={className ? `icon-button ${className}` : "icon-button"}
|
||||||
|
aria-label={label}
|
||||||
|
data-active={active}
|
||||||
|
{disabled}
|
||||||
|
{type}
|
||||||
|
>
|
||||||
<IconGlyph name={icon} size="sm" />
|
<IconGlyph name={icon} size="sm" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { render } from "svelte/server";
|
import { render } from "svelte/server";
|
||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
|
import Button from "./Button.svelte";
|
||||||
import DashboardFrame from "./DashboardFrame.svelte";
|
import DashboardFrame from "./DashboardFrame.svelte";
|
||||||
import FooterCell from "./FooterCell.svelte";
|
import FooterCell from "./FooterCell.svelte";
|
||||||
|
import IconButton from "./IconButton.svelte";
|
||||||
import ServiceRow from "./ServiceRow.svelte";
|
import ServiceRow from "./ServiceRow.svelte";
|
||||||
import TelemetryCard from "./TelemetryCard.svelte";
|
import TelemetryCard from "./TelemetryCard.svelte";
|
||||||
import { dashboardPreviewFixtures } from "../fixtures";
|
import { dashboardPreviewFixtures } from "../fixtures";
|
||||||
|
|
@ -94,4 +96,31 @@ describe("dashboard UI components", () => {
|
||||||
expect(withoutProgress.body).not.toContain("telemetry-card__bar");
|
expect(withoutProgress.body).not.toContain("telemetry-card__bar");
|
||||||
expect(withProgress.body).toContain("--metric-progress: 42%");
|
expect(withProgress.body).toContain("--metric-progress: 42%");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("base button controls forward native attributes", () => {
|
||||||
|
const button = render(Button, {
|
||||||
|
props: {
|
||||||
|
label: "Refresh",
|
||||||
|
id: "refresh-action",
|
||||||
|
class: "custom-action",
|
||||||
|
"aria-controls": "refresh-target",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const iconButton = render(IconButton, {
|
||||||
|
props: {
|
||||||
|
icon: "mdi:refresh",
|
||||||
|
label: "Refresh status",
|
||||||
|
id: "refresh-icon-action",
|
||||||
|
class: "custom-icon-action",
|
||||||
|
"aria-expanded": "false",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(button.body).toContain("id=\"refresh-action\"");
|
||||||
|
expect(button.body).toContain("class=\"ui-button custom-action ");
|
||||||
|
expect(button.body).toContain("aria-controls=\"refresh-target\"");
|
||||||
|
expect(iconButton.body).toContain("id=\"refresh-icon-action\"");
|
||||||
|
expect(iconButton.body).toContain("class=\"icon-button custom-icon-action ");
|
||||||
|
expect(iconButton.body).toContain("aria-expanded=\"false\"");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
33
src/lib/ui/stories/FocusPreview.svelte
Normal file
33
src/lib/ui/stories/FocusPreview.svelte
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount, tick, type Snippet } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
children,
|
||||||
|
selector = "button, a[href]",
|
||||||
|
}: {
|
||||||
|
children?: Snippet;
|
||||||
|
selector?: string;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let root: HTMLDivElement;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await tick();
|
||||||
|
root.querySelector<HTMLElement>(selector)?.focus();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="focus-preview" bind:this={root}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.focus-preview {
|
||||||
|
display: inline-grid;
|
||||||
|
padding: var(--ui-space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-preview :global(:is(a, button)) {
|
||||||
|
box-shadow: var(--ui-focus-ring);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script module lang="ts">
|
<script module lang="ts">
|
||||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||||
import IconButton from "../components/IconButton.svelte";
|
import IconButton from "../components/IconButton.svelte";
|
||||||
|
import FocusPreview from "./FocusPreview.svelte";
|
||||||
|
|
||||||
const { Story } = defineMeta({
|
const { Story } = defineMeta({
|
||||||
title: "Base UI/IconButton",
|
title: "Base UI/IconButton",
|
||||||
|
|
@ -14,4 +15,8 @@
|
||||||
<Story name="Inspect" args={{ icon: "mdi:magnify-scan", label: "Inspect" }} />
|
<Story name="Inspect" args={{ icon: "mdi:magnify-scan", label: "Inspect" }} />
|
||||||
<Story name="Disabled" args={{ icon: "mdi:refresh", label: "Disabled", disabled: true }} />
|
<Story name="Disabled" args={{ icon: "mdi:refresh", label: "Disabled", disabled: true }} />
|
||||||
<Story name="Active" args={{ icon: "mdi:refresh", label: "Active", active: true }} />
|
<Story name="Active" args={{ icon: "mdi:refresh", label: "Active", active: true }} />
|
||||||
<Story name="Focus Visible" args={{ icon: "mdi:keyboard", label: "Focus visible", active: true }} />
|
<Story name="Focus Visible" asChild>
|
||||||
|
<FocusPreview>
|
||||||
|
<IconButton icon="mdi:keyboard" label="Focus visible" />
|
||||||
|
</FocusPreview>
|
||||||
|
</Story>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script module lang="ts">
|
<script module lang="ts">
|
||||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||||
import ServiceRow from "../components/ServiceRow.svelte";
|
import ServiceRow from "../components/ServiceRow.svelte";
|
||||||
|
import FocusPreview from "./FocusPreview.svelte";
|
||||||
import { serviceRows } from "./story-data";
|
import { serviceRows } from "./story-data";
|
||||||
|
|
||||||
const { Story } = defineMeta({
|
const { Story } = defineMeta({
|
||||||
|
|
@ -16,4 +17,8 @@
|
||||||
<Story name="Stale" args={{ service: serviceRows.stale }} />
|
<Story name="Stale" args={{ service: serviceRows.stale }} />
|
||||||
<Story name="No Link" args={{ service: serviceRows.noLink }} />
|
<Story name="No Link" args={{ service: serviceRows.noLink }} />
|
||||||
<Story name="Long Title Description" args={{ service: serviceRows.long }} />
|
<Story name="Long Title Description" args={{ service: serviceRows.long }} />
|
||||||
<Story name="Keyboard Focus" args={{ service: serviceRows.normal }} />
|
<Story name="Keyboard Focus" asChild>
|
||||||
|
<FocusPreview selector="a">
|
||||||
|
<ServiceRow service={serviceRows.normal} />
|
||||||
|
</FocusPreview>
|
||||||
|
</Story>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { join } from "node:path";
|
||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
const root = process.cwd();
|
const root = process.cwd();
|
||||||
|
const componentsDir = join(root, "src/lib/ui/components");
|
||||||
const storiesDir = join(root, "src/lib/ui/stories");
|
const storiesDir = join(root, "src/lib/ui/stories");
|
||||||
|
|
||||||
const requiredStoryFiles = [
|
const requiredStoryFiles = [
|
||||||
|
|
@ -57,6 +58,16 @@ describe("Storybook inventory", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("keeps component and story files paired as the UI inventory changes", () => {
|
||||||
|
const componentStoryFiles = readdirSync(componentsDir)
|
||||||
|
.filter((filename) => filename.endsWith(".svelte"))
|
||||||
|
.map((filename) => filename.replace(".svelte", ".stories.svelte"));
|
||||||
|
|
||||||
|
for (const filename of componentStoryFiles) {
|
||||||
|
expect(existsSync(join(storiesDir, filename)), `${filename} is missing`).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("keeps Storybook fixtures generic and content-free", () => {
|
test("keeps Storybook fixtures generic and content-free", () => {
|
||||||
const storyText = readdirSync(storiesDir)
|
const storyText = readdirSync(storiesDir)
|
||||||
.filter((filename) => filename.endsWith(".svelte") || filename.endsWith(".ts"))
|
.filter((filename) => filename.endsWith(".svelte") || filename.endsWith(".ts"))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue