Drop page / section descriptions, state notes and repeated captions from the representative Stories of 44 components, the Patterns, Center Graphic and OrgChart pages. Value readouts, spec lists and captions hide only on the representative page; hidden verification Stories keep them. Tabs Default API and the Filter Bar draft / applied counts move to the hidden DefaultApi and DraftApplied Stories; Center Overview is hidden with its direct URL kept. Story IDs are unchanged. Align the component, pattern and OrgChart docs with the pages, point the quality reference check at the DraftApplied Story, and make the P8 styles check expect no section header on the preview-only Expanded page.
145 lines
5.9 KiB
TypeScript
145 lines
5.9 KiB
TypeScript
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 (
|
||
<div className="grid gap-3">
|
||
<div className="flex justify-between text-sm">
|
||
<span className="font-medium">{label}</span>
|
||
<span className="text-muted-foreground" data-testid="value">{valueText}</span>
|
||
</div>
|
||
{children}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function Single() {
|
||
const [value, setValue] = useState([40])
|
||
return (
|
||
<Labeled label="볼륨" valueText={`${value[0]}`}>
|
||
<Slider aria-label="볼륨" value={value} onValueChange={setValue} max={100} step={1} />
|
||
</Labeled>
|
||
)
|
||
}
|
||
|
||
function Range() {
|
||
const [value, setValue] = useState([25, 75])
|
||
return (
|
||
<Labeled label="가격 범위" valueText={`${value[0]} – ${value[1]}`}>
|
||
<Slider aria-label="가격 범위" value={value} onValueChange={setValue} max={100} step={1} minStepsBetweenThumbs={5} />
|
||
</Labeled>
|
||
)
|
||
}
|
||
|
||
function Step() {
|
||
const [value, setValue] = useState([50])
|
||
return (
|
||
<Labeled label="단계 (step 25)" valueText={`${value[0]}`}>
|
||
<Slider aria-label="단계" value={value} onValueChange={setValue} max={100} step={25} />
|
||
</Labeled>
|
||
)
|
||
}
|
||
|
||
function Disabled() {
|
||
return <Slider aria-label="비활성" defaultValue={[60]} disabled />
|
||
}
|
||
|
||
function Vertical() {
|
||
return (
|
||
<div style={{ height: 180 }} className="flex justify-center">
|
||
<Slider aria-label="세로" defaultValue={[30]} orientation="vertical" />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const sections = [
|
||
{ id: "single", label: "Single", node: <Single /> },
|
||
{ id: "range", label: "Range", node: <Range /> },
|
||
{ id: "step", label: "Step", node: <Step /> },
|
||
{ id: "disabled", label: "Disabled", node: <Disabled /> },
|
||
{ id: "vertical", label: "Vertical", node: <Vertical /> },
|
||
]
|
||
|
||
const meta = { title: "Components/Form/Slider", component: Slider, parameters: { layout: "padded" } } satisfies Meta<typeof Slider>
|
||
export default meta
|
||
type Story = StoryObj<typeof meta>
|
||
|
||
const board = (...ids: string[]) => (
|
||
<StateBoard min={320} states={sections.filter((s) => ids.includes(s.id)).map((s) => ({ id: s.id, label: s.label, children: s.node }))} />
|
||
)
|
||
|
||
// Representative Story: the common Story page; the sections group the same specimens.
|
||
export const All: Story = {
|
||
name: "Slider",
|
||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||
render: () => (
|
||
<StoryPage>
|
||
<StoryPageHeader title="Slider" />
|
||
<StorySection title="Overview">{board("single")}</StorySection>
|
||
<StorySection title="Variants">{board("range", "step", "vertical")}</StorySection>
|
||
<StorySection title="States">{board("disabled")}</StorySection>
|
||
</StoryPage>
|
||
),
|
||
}
|
||
|
||
export const SingleValue: Story = { tags: ["verification"], name: "Single", render: () => <Single /> }
|
||
export const RangeValue: Story = { tags: ["verification"], name: "Range", render: () => <Range /> }
|
||
export const DisabledSlider: Story = { tags: ["verification"], name: "Disabled", render: () => <Disabled /> }
|
||
|
||
export const Focus: Story = {
|
||
tags: ["verification"],
|
||
render: () => (
|
||
<div className="grid gap-8">
|
||
<Single />
|
||
<Labeled label="비활성" valueText="60"><Disabled /></Labeled>
|
||
<output data-testid="slider-focus-result" hidden>Ready</output>
|
||
</div>
|
||
),
|
||
play: async ({ canvasElement }) => {
|
||
const thumb = canvasElement.querySelector<HTMLElement>('[data-slot="slider-thumb"]')!
|
||
const disabled = canvasElement.querySelector<HTMLElement>('[data-disabled] [data-slot="slider-thumb"]')!
|
||
const result = canvasElement.querySelector<HTMLOutputElement>('[data-testid="slider-focus-result"]')!
|
||
const states: Record<string, string> = {}
|
||
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)
|
||
},
|
||
}
|