Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
Svelte
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228<script module lang="ts"> import { defineMeta } from "@storybook/addon-svelte-csf"; import { expect, fireEvent, fn, waitFor } from "storybook/test"; import ChevronRight from "$icon/chevron-right"; import GitFork from "$icon/git-fork"; import Star from "$icon/star"; import Button from "$lib/components/ui/Button.svelte";
const { Story } = defineMeta({ title: "UI/Button", component: Button, tags: ["autodocs"], argTypes: { variant: { control: { type: "select" }, options: ["default", "primary", "ghost", "danger", "success", "warning"] }, size: { control: { type: "inline-radio" }, options: ["small", "regular", "large"] }, iconSide: { control: { type: "inline-radio" }, options: ["left", "right"] }, insetShadow: { control: "boolean" }, disabled: { control: "boolean" }, loading: { control: "boolean" }, holdToConfirm: { control: "boolean" } }, args: { variant: "default", size: "regular", iconSide: "left", insetShadow: false, disabled: false, loading: false } });</script>
<!-- Button content comes through its `children` snippet, provided here as Story slot content. --><Story name="Default">Button</Story><Story name="Primary" args={{ variant: "primary" }}>Save</Story><Story name="Ghost" args={{ variant: "ghost" }}>Cancel</Story><Story name="Danger" args={{ variant: "danger" }}>Delete</Story><Story name="Success" args={{ variant: "success" }}>Confirm</Story><Story name="Warning" args={{ variant: "warning" }}>Careful</Story><Story name="Loading" args={{ variant: "primary", loading: true }}>Saving</Story><Story name="Disabled" args={{ disabled: true }}>Button</Story><Story name="Link" args={{ href: "/" }}>Go home</Story>
<Story name="Hold to confirm" args={{ holdToConfirm: true, onconfirm: fn(), onclick: fn() }} play={async ({ canvas, args, userEvent }) => { const button = canvas.getByRole("button", { name: "Hold to confirm" }); const capture = button.setPointerCapture; button.setPointerCapture = fn(); try { await userEvent.click(button); await userEvent.pointer({ target: button, keys: "[MouseLeft>]" }); await new Promise((resolve) => setTimeout(resolve, 100)); await userEvent.pointer({ target: button, keys: "[/MouseLeft]" }); await new Promise((resolve) => setTimeout(resolve, 1900)); await expect(args.onconfirm).not.toHaveBeenCalled(); await expect(args.onclick).not.toHaveBeenCalled(); } finally { button.setPointerCapture = capture; } }}> Hold to confirm</Story><Story name="Hold danger" args={{ holdToConfirm: true, variant: "danger", onconfirm: fn(), onclick: fn() }} play={async ({ canvas, args, userEvent }) => { const button = canvas.getByRole("button", { name: "Hold to delete" }); const capture = button.setPointerCapture; button.setPointerCapture = fn(); try { const bounds = button.getBoundingClientRect(); await userEvent.pointer({ target: button, keys: "[MouseLeft>]" }); await waitFor(() => expect(args.onconfirm).toHaveBeenCalledTimes(1), { timeout: 3000 }); await expect(button).toHaveAttribute("data-confirmed", "true"); await expect(canvas.getByRole("status")).toHaveTextContent("Confirmed"); await expect(button.getBoundingClientRect().width).toBe(bounds.width); await expect(button.getBoundingClientRect().height).toBe(bounds.height); await userEvent.pointer({ target: button, keys: "[/MouseLeft]" }); await userEvent.click(button); await expect(args.onconfirm).toHaveBeenCalledTimes(1); await expect(args.onclick).not.toHaveBeenCalled(); await waitFor(() => expect(button).toHaveAttribute("data-confirmed", "false"), { timeout: 2500 }); await expect(args.onconfirm).toHaveBeenCalledTimes(1); } finally { button.setPointerCapture = capture; } }}> Hold to delete</Story><Story name="Hold keyboard" args={{ holdToConfirm: true, onconfirm: fn(), onclick: fn() }} play={async ({ canvas, args }) => { const button = canvas.getByRole("button", { name: "Hold to confirm" }); button.focus(); fireEvent.keyDown(button, { key: " " }); await waitFor(() => expect(args.onconfirm).toHaveBeenCalledTimes(1), { timeout: 3000 }); fireEvent.keyDown(button, { key: " ", repeat: true }); fireEvent.keyUp(button, { key: " " }); fireEvent.click(button); await expect(args.onclick).not.toHaveBeenCalled(); await waitFor(() => expect(button).toHaveAttribute("data-confirmed", "false"), { timeout: 2500 }); await expect(args.onconfirm).toHaveBeenCalledTimes(1); }}> Hold to confirm</Story><Story name="Hold Enter" args={{ holdToConfirm: true, onconfirm: fn(), onclick: fn() }} play={async ({ canvas, args }) => { const button = canvas.getByRole("button", { name: "Hold to confirm" }); button.focus(); fireEvent.keyDown(button, { key: "Enter" }); await waitFor(() => expect(args.onconfirm).toHaveBeenCalledTimes(1), { timeout: 3000 }); fireEvent.keyDown(button, { key: "Enter", repeat: true }); fireEvent.keyUp(button, { key: "Enter" }); fireEvent.click(button); await expect(args.onconfirm).toHaveBeenCalledTimes(1); await expect(args.onclick).not.toHaveBeenCalled(); }}> Hold to confirm</Story><Story name="Hold Escape" args={{ holdToConfirm: true, onconfirm: fn() }} play={async ({ canvas, args }) => { const button = canvas.getByRole("button", { name: "Hold to confirm" }); button.focus(); fireEvent.keyDown(button, { key: " " }); await new Promise((resolve) => setTimeout(resolve, 900)); fireEvent.keyDown(button, { key: "Escape" }); fireEvent.keyUp(button, { key: " " }); fireEvent.keyDown(button, { key: "Enter" }); await new Promise((resolve) => setTimeout(resolve, 1100)); fireEvent.keyUp(button, { key: "Enter" }); await expect(args.onconfirm).not.toHaveBeenCalled(); }}> Hold to confirm</Story><Story name="Hold disabled" args={{ holdToConfirm: true, variant: "danger", disabled: true, onconfirm: fn(), onclick: fn() }} play={async ({ canvas, args, userEvent }) => { const button = canvas.getByRole("button", { name: "Hold to delete" }); await expect(button).toBeDisabled(); fireEvent.pointerDown(button, { pointerId: 1, isPrimary: true, button: 0 }); fireEvent.keyDown(button, { key: "Enter" }); await userEvent.click(button); await new Promise((resolve) => setTimeout(resolve, 1900)); await expect(args.onconfirm).not.toHaveBeenCalled(); await expect(args.onclick).not.toHaveBeenCalled(); }}> Hold to delete</Story><Story name="Hold loading" args={{ holdToConfirm: true, variant: "danger", loading: true, onconfirm: fn() }}> Hold to delete</Story>
<Story name="Inset shadow" asChild> <div class="flex items-center gap-sm"> <Button insetShadow>Button</Button> <Button variant="primary" insetShadow>Save</Button> </div></Story>
<Story name="Sizes" asChild> <div class="flex items-center gap-sm"> <Button size="mini">Mini</Button> <Button size="small">Small</Button> <Button size="regular">Regular</Button> <Button size="large">Large</Button> </div></Story>
<Story name="With icon" args={{ icon: Star }}>Star</Story><Story name="Icon right" args={{ icon: ChevronRight, iconSide: "right" }}>Next</Story><Story name="Icon only" args={{ icon: Star, "aria-label": "Star" }} />
<!-- the icon rides the same gap and scales with the size variant. --><Story name="Icon sizes" asChild> <div class="flex items-center gap-sm"> <Button size="mini" icon={Star}>Mini</Button> <Button size="small" icon={Star}>Small</Button> <Button size="regular" icon={Star}>Regular</Button> <Button size="large" icon={Star}>Large</Button> </div></Story>
<!-- press these: the icon and label sink together while the button box stays put. --><Story name="Icons with inset shadow" asChild> <div class="flex items-center gap-sm"> <Button insetShadow icon={Star}>Star</Button> <Button variant="primary" insetShadow icon={GitFork}>Fork</Button> <Button insetShadow icon={ChevronRight} iconSide="right">Next</Button> <Button insetShadow icon={Star} aria-label="Star" /> </div></Story>