import { useState, type ReactNode } from "react" import type { Meta, StoryObj } from "@storybook/react-vite" import { Slider } from "@hm-design/ui" import { StateBoard } from "@/components/story-board" import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" // The representative Story shows each Slider mode with real, draggable controls; the same specimens also exist as Stories tagged // `verification` (hidden from the sidebar). The value label is the Story's composition (Slider has no built-in label). // Names (`aria-label`) are provided by the consumer. function Labeled({ label, children, valueText }: { label: string; children: ReactNode; valueText: string }) { return (
{label} {valueText}
{children}
) } function Single() { const [value, setValue] = useState([40]) return ( ) } function Range() { const [value, setValue] = useState([25, 75]) return ( ) } function Step() { const [value, setValue] = useState([50]) return ( ) } function Disabled() { return } function Vertical() { return (
) } const sections = [ { id: "single", label: "Single", node: }, { id: "range", label: "Range", node: }, { id: "step", label: "Step", node: }, { id: "disabled", label: "Disabled", node: }, { id: "vertical", label: "Vertical", node: }, ] const meta = { title: "Components/Form/Slider", component: Slider, parameters: { layout: "padded" } } satisfies Meta export default meta type Story = StoryObj // Representative-page layout (Storybook presentation only): 4 equal columns on PC (iframe width 1000px and up), shrinking to // fit below that (200px minimum per column). Each specimen sits in its own single-column StateBoard (min larger than any // column), so StateBoard's auto-fill grid cannot leave an empty or stretched track. A section with fewer than 4 specimens // keeps the same column width and leaves the remaining tracks empty. function SpecimenGrid({ ids }: { ids: string[] }) { return (
{ids.map((id) => { const s = sections.find((section) => section.id === id)! return })}
) } // Representative Story: the common Story page; the sections group the same specimens. // Overview is merged into Variants (Single is its first item). `Vertical` centers its Slider in its column. export const All: Story = { name: "Slider", parameters: { layout: "fullscreen", controls: { disable: true } }, render: () => ( ), } export const SingleValue: Story = { tags: ["verification"], name: "Single", render: () => } export const RangeValue: Story = { tags: ["verification"], name: "Range", render: () => } export const DisabledSlider: Story = { tags: ["verification"], name: "Disabled", render: () => } export const Focus: Story = { tags: ["verification"], render: () => (
), play: async ({ canvasElement }) => { const thumb = canvasElement.querySelector('[data-slot="slider-thumb"]')! const disabled = canvasElement.querySelector('[data-disabled] [data-slot="slider-thumb"]')! const result = canvasElement.querySelector('[data-testid="slider-focus-result"]')! const states: Record = {} const read = (name: string, element = thumb) => { states[name] = getComputedStyle(element).boxShadow } const assert = (condition: boolean, message: string) => { if (!condition) throw Error(message) } thumb.blur() read("Default") // Existing Storybook pseudo-state mirroring preserves production selector order/specificity. thumb.setAttribute("data-sb-hover", "") read("Hover") thumb.setAttribute("data-sb-active", "") read("Drag") thumb.removeAttribute("data-sb-hover") thumb.removeAttribute("data-sb-active") thumb.focus() assert(thumb.matches(":focus-visible"), "Keyboard focus must be visible") read("Focus") thumb.setAttribute("data-sb-hover", "") read("Focus + Hover") thumb.setAttribute("data-sb-active", "") read("Focus + Drag") disabled.setAttribute("data-sb-hover", "") disabled.setAttribute("data-sb-active", "") disabled.focus() read("Disabled", disabled) assert(document.activeElement !== disabled, "Disabled thumb cannot receive focus") assert(states.Default === "none" && states.Disabled === "none", "Default/Disabled have no halo") assert(states.Hover.includes("4px") && states.Drag === states.Hover.replace("4px", "6px"), "Interaction keeps 4/6px and its color") assert(states.Focus.includes("4px") && states["Focus + Hover"] === states.Focus, "Hover cannot mask Focus") assert(states["Focus + Drag"] === states.Focus.replace("4px", "6px"), "Drag keeps 6px without masking Focus color") for (const element of [thumb, disabled]) { element.removeAttribute("data-sb-hover") element.removeAttribute("data-sb-active") } thumb.focus() result.textContent = JSON.stringify(states) }, }