From 3f364e15330113f25f6275a5d0ce0fc190128503 Mon Sep 17 00:00:00 2001 From: vpolessky Date: Wed, 29 Jul 2026 12:20:32 +0200 Subject: [PATCH 1/2] feat(design-system): add DsSlider_v01 component [AR-77548] --- .changeset/clever-carrots-invite.md | 5 + .../__tests__/ds-slider.browser.test.tsx | 89 +++++++++++++ .../components/ds-slider/ds-slider.figma.ts | 29 ++++ .../ds-slider/ds-slider.module.scss | 102 ++++++++++++++ .../ds-slider/ds-slider.stories.tsx | 120 +++++++++++++++++ .../src/components/ds-slider/ds-slider.tsx | 125 ++++++++++++++++++ .../components/ds-slider/ds-slider.types.ts | 99 ++++++++++++++ .../src/components/ds-slider/index.ts | 2 + packages/design-system/src/index.ts | 1 + 9 files changed, 572 insertions(+) create mode 100644 .changeset/clever-carrots-invite.md create mode 100644 packages/design-system/src/components/ds-slider/__tests__/ds-slider.browser.test.tsx create mode 100644 packages/design-system/src/components/ds-slider/ds-slider.figma.ts create mode 100644 packages/design-system/src/components/ds-slider/ds-slider.module.scss create mode 100644 packages/design-system/src/components/ds-slider/ds-slider.stories.tsx create mode 100644 packages/design-system/src/components/ds-slider/ds-slider.tsx create mode 100644 packages/design-system/src/components/ds-slider/ds-slider.types.ts create mode 100644 packages/design-system/src/components/ds-slider/index.ts diff --git a/.changeset/clever-carrots-invite.md b/.changeset/clever-carrots-invite.md new file mode 100644 index 000000000..ae73f9c92 --- /dev/null +++ b/.changeset/clever-carrots-invite.md @@ -0,0 +1,5 @@ +--- +'@drivenets/design-system': minor +--- + +Add `DsSlider` component diff --git a/packages/design-system/src/components/ds-slider/__tests__/ds-slider.browser.test.tsx b/packages/design-system/src/components/ds-slider/__tests__/ds-slider.browser.test.tsx new file mode 100644 index 000000000..438376ead --- /dev/null +++ b/packages/design-system/src/components/ds-slider/__tests__/ds-slider.browser.test.tsx @@ -0,0 +1,89 @@ +import { useState } from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { page, userEvent } from 'vitest/browser'; +import DsSlider from '../ds-slider'; +import type { DsSliderValue } from '../ds-slider.types'; + +function focus(locator: ReturnType) { + (locator.element() as HTMLElement).focus(); +} + +describe('DsSlider', () => { + it('renders the label and the numeric output', async () => { + await page.render(); + + await expect.element(page.getByText('Input label')).toBeVisible(); + await expect + .element(page.getByRole('slider', { name: /Input label/i })) + .toHaveAttribute('aria-valuenow', '36'); + }); + + it('steps the value with the keyboard and reports the change', async () => { + const onValueChange = vi.fn(); + + await page.render(); + + const slider = page.getByRole('slider', { name: /Level/i }); + focus(slider); + + await userEvent.keyboard('{ArrowRight}'); + await expect.element(slider).toHaveAttribute('aria-valuenow', '37'); + await expect.element(page.getByText('37')).toBeVisible(); + expect(onValueChange).toHaveBeenLastCalledWith(37); + + await userEvent.keyboard('{End}'); + await expect.element(slider).toHaveAttribute('aria-valuenow', '100'); + expect(onValueChange).toHaveBeenLastCalledWith(100); + }); + + it('renders two clamped thumbs and a range output for type="range"', async () => { + await page.render( + , + ); + + const min = page.getByRole('slider', { name: 'Minimum' }); + const max = page.getByRole('slider', { name: 'Maximum' }); + + await expect.element(min).toHaveAttribute('aria-valuenow', '36'); + await expect.element(max).toHaveAttribute('aria-valuenow', '60'); + await expect.element(page.getByText('36-60')).toBeVisible(); + }); + + it('does not change when disabled', async () => { + const onValueChange = vi.fn(); + + await page.render(); + + const slider = page.getByRole('slider', { name: /Level/i }); + focus(slider); + await userEvent.keyboard('{ArrowRight}'); + + await expect.element(slider).toHaveAttribute('aria-valuenow', '36'); + expect(onValueChange).not.toHaveBeenCalled(); + }); + + it('respects a controlled value', async () => { + function Controlled() { + const [value, setValue] = useState(20); + + return ; + } + + await page.render(); + + const slider = page.getByRole('slider', { name: /Volume/i }); + await expect.element(slider).toHaveAttribute('aria-valuenow', '20'); + + focus(slider); + await userEvent.keyboard('{ArrowRight}'); + await expect.element(slider).toHaveAttribute('aria-valuenow', '21'); + }); + + it('formats the output with formatValue', async () => { + await page.render( + `${String(current)}%`} />, + ); + + await expect.element(page.getByText('40%')).toBeVisible(); + }); +}); diff --git a/packages/design-system/src/components/ds-slider/ds-slider.figma.ts b/packages/design-system/src/components/ds-slider/ds-slider.figma.ts new file mode 100644 index 000000000..342d94518 --- /dev/null +++ b/packages/design-system/src/components/ds-slider/ds-slider.figma.ts @@ -0,0 +1,29 @@ +// url=https://www.figma.com/design/nha3m67y7S57cHCSuQO2gp/DAP-Design-System-1.2?node-id=29879-165 +// source=https://github.com/drivenets/design-system/tree/main/packages/design-system/src/components/ds-slider +// component=DsSlider +import figma from 'figma'; + +const instance = figma.selectedInstance; + +const type = instance.getEnum('type', { + value: 'value', + range: 'range', +}); + +const isRange = type === 'range'; + +const attrs = [ + 'label="Input label"', + isRange ? 'type="range"' : '', + isRange ? 'defaultValue={[36, 60]}' : 'defaultValue={36}', + isRange ? "thumbLabels={['Minimum', 'Maximum']}" : '', +] + .filter(Boolean) + .join(' '); + +export default { + example: figma.code``, + imports: ["import { DsSlider } from '@drivenets/design-system';"], + id: 'ds-slider', + metadata: { nestable: false }, +} satisfies figma.Template; diff --git a/packages/design-system/src/components/ds-slider/ds-slider.module.scss b/packages/design-system/src/components/ds-slider/ds-slider.module.scss new file mode 100644 index 000000000..1e14802aa --- /dev/null +++ b/packages/design-system/src/components/ds-slider/ds-slider.module.scss @@ -0,0 +1,102 @@ +.root { + --ds-slider-thumb-size: var(--md); + --ds-slider-thumb-size-active: var(--lg); + --ds-slider-thumb-border: 2px; + --ds-slider-thumb-border-active: 4px; + --ds-slider-track-height: 4px; + --ds-slider-focus-ring: 3px; + + display: flex; + flex-direction: column; + gap: var(--sm); + width: 100%; + + &.small { + --ds-slider-thumb-size: var(--standard); + --ds-slider-thumb-size-active: var(--md); + --ds-slider-track-height: 2px; + } + + &.large { + --ds-slider-thumb-size: var(--lg); + --ds-slider-thumb-size-active: 28px; + --ds-slider-thumb-border: 3px; + --ds-slider-thumb-border-active: 5px; + --ds-slider-track-height: 8px; + } + + &[data-disabled] { + cursor: not-allowed; + + .range { + background: var(--background-disable); + } + + .thumb { + cursor: not-allowed; + border-color: var(--background-disable); + } + } +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--xs); + padding-bottom: var(--4xs); +} + +.valueText { + margin-left: auto; + text-align: right; +} + +.control { + position: relative; + display: flex; + align-items: center; + min-height: var(--ds-slider-thumb-size-active); +} + +.track { + flex: 1; + height: var(--ds-slider-track-height); + background: var(--background-secondary-hover); + border-radius: 9999px; +} + +.range { + height: 100%; + background: var(--background-primary); + border-radius: 9999px; +} + +.thumb { + box-sizing: border-box; + width: var(--ds-slider-thumb-size); + height: var(--ds-slider-thumb-size); + background: var(--background); + border: var(--ds-slider-thumb-border) solid var(--background-primary); + border-radius: 9999px; + outline: none; + cursor: pointer; + transition: + width 0.2s, + height 0.2s, + border-width 0.2s, + border-color 0.2s, + box-shadow 0.2s; + + &:not([data-disabled]):hover, + &:focus-visible { + width: var(--ds-slider-thumb-size-active); + height: var(--ds-slider-thumb-size-active); + border-width: var(--ds-slider-thumb-border-active); + border-color: var(--background-primary-hover); + } + + &:focus-visible { + box-shadow: 0 0 0 var(--ds-slider-focus-ring) var(--background-secondary-hover); + } +} diff --git a/packages/design-system/src/components/ds-slider/ds-slider.stories.tsx b/packages/design-system/src/components/ds-slider/ds-slider.stories.tsx new file mode 100644 index 000000000..1b7c3226f --- /dev/null +++ b/packages/design-system/src/components/ds-slider/ds-slider.stories.tsx @@ -0,0 +1,120 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import DsSlider from './ds-slider'; +import { sliderSizes, sliderTypes, type DsSliderValue } from './ds-slider.types'; + +const meta: Meta = { + title: 'Components/Slider', + component: DsSlider, + parameters: { + layout: 'padded', + }, + argTypes: { + type: { + control: 'inline-radio', + options: sliderTypes, + description: 'Single value or a [min, max] interval', + }, + size: { + control: 'inline-radio', + options: sliderSizes, + description: 'Drives thumb and track sizing together', + }, + label: { control: 'text' }, + min: { control: 'number' }, + max: { control: 'number' }, + step: { control: 'number' }, + disabled: { control: 'boolean' }, + className: { table: { disable: true }, control: false }, + style: { table: { disable: true }, control: false }, + ref: { table: { disable: true }, control: false }, + onValueChange: { table: { disable: true }, control: false }, + onValueChangeEnd: { table: { disable: true }, control: false }, + }, +}; + +export default meta; + +type Story = StoryObj; + +/** + * A single-thumb slider selects one number from a bounded range. The output above the + * track reflects the current value live while dragging. + */ +export const Default: Story = { + args: { + label: 'Input label', + type: 'value', + defaultValue: 36, + min: 0, + max: 100, + }, +}; + +/** + * A range slider uses two thumbs to select a `[min, max]` interval; the thumbs cannot + * cross. Provide `thumbLabels` so each thumb has its own accessible name. + */ +export const Range: Story = { + args: { + label: 'Input label', + type: 'range', + defaultValue: [36, 60], + min: 0, + max: 100, + thumbLabels: ['Minimum', 'Maximum'], + }, +}; + +/** + * Size drives thumb and track thickness together — use `small` in dense layouts. + */ +export const Small: Story = { + args: { + label: 'Input label', + size: 'small', + defaultValue: 36, + }, +}; + +/** + * The `large` size gives a bigger hit target and a thicker track for prominent controls. + */ +export const Large: Story = { + args: { + label: 'Input label', + size: 'large', + defaultValue: 36, + }, +}; + +/** + * Disabled applies to the whole control, including the label row and numeric output. + */ +export const Disabled: Story = { + args: { + label: 'Input label', + defaultValue: 36, + disabled: true, + }, +}; + +/** + * Drive the value from external state via `value` + `onValueChange`. The output can be + * formatted with `formatValue` (here as a percentage). + */ +export const Controlled: Story = { + parameters: { docs: { source: { type: 'code' } } }, + render: function Render() { + const [value, setValue] = useState(40); + + return ( + `${String(current)}%`} + /> + ); + }, +}; diff --git a/packages/design-system/src/components/ds-slider/ds-slider.tsx b/packages/design-system/src/components/ds-slider/ds-slider.tsx new file mode 100644 index 000000000..ee5bba2cb --- /dev/null +++ b/packages/design-system/src/components/ds-slider/ds-slider.tsx @@ -0,0 +1,125 @@ +import { Slider, type SliderValueChangeDetails } from '@ark-ui/react/slider'; +import classNames from 'classnames'; +import { useControlled } from '../../utils/use-controlled'; +import { DsTypography } from '../ds-typography'; +import type { DsSliderProps, DsSliderValue } from './ds-slider.types'; +import styles from './ds-slider.module.scss'; + +const toArray = (value: DsSliderValue | undefined): number[] | undefined => { + if (value === undefined) { + return undefined; + } + + return Array.isArray(value) ? value : [value]; +}; + +/** + * @summary Selects a numeric value or a numeric interval by dragging thumbs along a track. + * + * Use for picking a number or interval from a known continuous range where visual + * adjustment beats typing (filter ranges, thresholds, capacity limits). For exact + * numeric entry use DsNumberInput; for a few discrete options use DsSegmentGroup. + */ +const DsSlider = ({ + ref, + className, + style, + label, + type = 'value', + size = 'medium', + min = 0, + max = 100, + step = 1, + value, + defaultValue, + name, + disabled = false, + thumbLabels, + formatValue = String, + onValueChange, + onValueChangeEnd, +}: DsSliderProps) => { + const isRange = type === 'range'; + + const controlledValue = toArray(value); + const fallbackValue = isRange ? [min, max] : [min]; + + const toPublicValue = (next: number[]): DsSliderValue => + isRange ? [next[0] ?? min, next[1] ?? max] : (next[0] ?? min); + + const [values, setValues] = useControlled( + controlledValue, + controlledValue !== undefined ? (next) => onValueChange?.(toPublicValue(next)) : undefined, + toArray(defaultValue) ?? fallbackValue, + ); + + const handleValueChange = (details: SliderValueChangeDetails) => { + setValues(details.value); + + if (controlledValue === undefined) { + onValueChange?.(toPublicValue(details.value)); + } + }; + + const valueText = isRange + ? `${formatValue(values[0] ?? min)}-${formatValue(values[1] ?? max)}` + : formatValue(values[0] ?? min); + + const textColor = disabled ? 'disabled' : 'main'; + + const thumbAriaLabels = isRange ? thumbLabels : label ? [label] : thumbLabels; + + const rootClass = classNames( + styles.root, + { + [styles.small]: size === 'small', + [styles.large]: size === 'large', + }, + className, + ); + + return ( + onValueChangeEnd?.(toPublicValue(details.value))} + > +
+ {label && ( + + {label} + + )} + + + {valueText} + +
+ + + + + + + {values.map((_, index) => ( + + + + ))} + +
+ ); +}; + +DsSlider.displayName = 'DsSlider'; + +export default DsSlider; diff --git a/packages/design-system/src/components/ds-slider/ds-slider.types.ts b/packages/design-system/src/components/ds-slider/ds-slider.types.ts new file mode 100644 index 000000000..353288f23 --- /dev/null +++ b/packages/design-system/src/components/ds-slider/ds-slider.types.ts @@ -0,0 +1,99 @@ +import type { CSSProperties, Ref } from 'react'; + +export const sliderTypes = ['value', 'range'] as const; +export type DsSliderType = (typeof sliderTypes)[number]; + +export const sliderSizes = ['small', 'medium', 'large'] as const; +export type DsSliderSize = (typeof sliderSizes)[number]; + +/** + * Value for a `value` slider (single number) or a `range` slider ([min, max] tuple). + */ +export type DsSliderValue = number | [number, number]; + +/** + * Props for the DsSlider component. + */ +export type DsSliderProps = { + /** + * Forwarded ref for the slider root element. + */ + ref?: Ref; + + className?: string; + + style?: CSSProperties; + + /** + * Visible label rendered above the track. Becomes the accessible name of the control. + */ + label?: string; + + /** + * Selection mode: a single value or a `[min, max]` interval. + * @default 'value' + */ + type?: DsSliderType; + + /** + * Drives thumb and track sizing together (S/M/L → 16/20/24px thumb, 2/4/8px track). + * @default 'medium' + */ + size?: DsSliderSize; + + /** + * Lowest selectable value. + * @default 0 + */ + min?: number; + + /** + * Highest selectable value. + * @default 100 + */ + max?: number; + + /** + * Granularity that the value snaps to. Must be greater than 0. + * @default 1 + */ + step?: number; + + /** + * Controlled value. A number for `type="value"`, a `[min, max]` tuple for `type="range"`. + */ + value?: DsSliderValue; + + /** + * Uncontrolled initial value. Defaults to `min` for `value` and `[min, max]` for `range`. + */ + defaultValue?: DsSliderValue; + + /** + * Form field name applied to the hidden inputs. + */ + name?: string; + + disabled?: boolean; + + /** + * Accessible name per thumb. Required for `type="range"` (e.g. `['Minimum', 'Maximum']`). + */ + thumbLabels?: string[]; + + /** + * Formats each number shown in the output above the track. Range values are joined with `-`. + * @default String + */ + formatValue?: (value: number) => string; + + /** + * Called live while the value changes (drag or keyboard). + */ + onValueChange?: (value: DsSliderValue) => void; + + /** + * Called once an interaction settles (pointer released or key committed). + */ + onValueChangeEnd?: (value: DsSliderValue) => void; +}; diff --git a/packages/design-system/src/components/ds-slider/index.ts b/packages/design-system/src/components/ds-slider/index.ts new file mode 100644 index 000000000..340528817 --- /dev/null +++ b/packages/design-system/src/components/ds-slider/index.ts @@ -0,0 +1,2 @@ +export { default as DsSlider } from './ds-slider'; +export type { DsSliderProps, DsSliderValue, DsSliderType, DsSliderSize } from './ds-slider.types'; diff --git a/packages/design-system/src/index.ts b/packages/design-system/src/index.ts index 54077328c..dd38b6e05 100644 --- a/packages/design-system/src/index.ts +++ b/packages/design-system/src/index.ts @@ -45,6 +45,7 @@ export * from './components/ds-segment-group'; export * from './components/ds-select'; export * from './components/ds-site-node'; export * from './components/ds-skeleton'; +export * from './components/ds-slider'; export * from './components/ds-smart-tabs'; export * from './components/ds-spinner'; export * from './components/ds-split-button'; From a4a66376ac67822e02a5b37fd67a2bbdf09ebe71 Mon Sep 17 00:00:00 2001 From: vpolessky Date: Wed, 29 Jul 2026 13:05:25 +0200 Subject: [PATCH 2/2] feat(design-system): fix comments [AR-77548] --- .../__snapshots__/ds-slider.docs.snap | 109 ++++++++++++++++++ .../ds-slider/ds-slider.module.scss | 18 +-- .../ds-slider/ds-slider.stories.tsx | 2 +- .../src/components/ds-slider/ds-slider.tsx | 9 +- .../components/ds-slider/ds-slider.types.ts | 3 + .../components/ds-slider/ds-slider.utils.ts | 14 +++ .../src/components/ds-slider/index.ts | 13 ++- .../storybook/docs-snippets.docs.test.ts | 1 + 8 files changed, 150 insertions(+), 19 deletions(-) create mode 100644 packages/design-system/src/components/ds-slider/__tests__/__snapshots__/ds-slider.docs.snap create mode 100644 packages/design-system/src/components/ds-slider/ds-slider.utils.ts diff --git a/packages/design-system/src/components/ds-slider/__tests__/__snapshots__/ds-slider.docs.snap b/packages/design-system/src/components/ds-slider/__tests__/__snapshots__/ds-slider.docs.snap new file mode 100644 index 000000000..2c69f8059 --- /dev/null +++ b/packages/design-system/src/components/ds-slider/__tests__/__snapshots__/ds-slider.docs.snap @@ -0,0 +1,109 @@ +# DsSlider docs snippets + +## Default + +### Show code + + +### MCP manifest +const Default = () => ; + +## Range + +### Show code + + +### MCP manifest +const Range = () => ; + +## Small + +### Show code + + +### MCP manifest +const Small = () => ; + +## Large + +### Show code + + +### MCP manifest +const Large = () => ; + +## Disabled + +### Show code + + +### MCP manifest +const Disabled = () => ; + +## Controlled + +### Show code +{ + parameters: { + docs: { + source: { + type: 'code' + } + } + }, + render: function Render() { + const [value, setValue] = useState(40); + return `${String(current)}%`} />; + } +} + +### MCP manifest +const Controlled = function Render() { + const [value, setValue] = useState(40); + + return ( + `${String(current)}%`} + /> + ); +}; \ No newline at end of file diff --git a/packages/design-system/src/components/ds-slider/ds-slider.module.scss b/packages/design-system/src/components/ds-slider/ds-slider.module.scss index 1e14802aa..fc764b0b1 100644 --- a/packages/design-system/src/components/ds-slider/ds-slider.module.scss +++ b/packages/design-system/src/components/ds-slider/ds-slider.module.scss @@ -5,6 +5,8 @@ --ds-slider-thumb-border-active: 4px; --ds-slider-track-height: 4px; --ds-slider-focus-ring: 3px; + --ds-slider-radius: 9999px; + --ds-slider-transition: 0.2s; display: flex; flex-direction: column; @@ -63,13 +65,13 @@ flex: 1; height: var(--ds-slider-track-height); background: var(--background-secondary-hover); - border-radius: 9999px; + border-radius: var(--ds-slider-radius); } .range { height: 100%; background: var(--background-primary); - border-radius: 9999px; + border-radius: var(--ds-slider-radius); } .thumb { @@ -78,15 +80,15 @@ height: var(--ds-slider-thumb-size); background: var(--background); border: var(--ds-slider-thumb-border) solid var(--background-primary); - border-radius: 9999px; + border-radius: var(--ds-slider-radius); outline: none; cursor: pointer; transition: - width 0.2s, - height 0.2s, - border-width 0.2s, - border-color 0.2s, - box-shadow 0.2s; + width var(--ds-slider-transition), + height var(--ds-slider-transition), + border-width var(--ds-slider-transition), + border-color var(--ds-slider-transition), + box-shadow var(--ds-slider-transition); &:not([data-disabled]):hover, &:focus-visible { diff --git a/packages/design-system/src/components/ds-slider/ds-slider.stories.tsx b/packages/design-system/src/components/ds-slider/ds-slider.stories.tsx index 1b7c3226f..eb76e6c7e 100644 --- a/packages/design-system/src/components/ds-slider/ds-slider.stories.tsx +++ b/packages/design-system/src/components/ds-slider/ds-slider.stories.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react-vite'; -import DsSlider from './ds-slider'; +import { DsSlider } from './index'; import { sliderSizes, sliderTypes, type DsSliderValue } from './ds-slider.types'; const meta: Meta = { diff --git a/packages/design-system/src/components/ds-slider/ds-slider.tsx b/packages/design-system/src/components/ds-slider/ds-slider.tsx index ee5bba2cb..2fdb2beee 100644 --- a/packages/design-system/src/components/ds-slider/ds-slider.tsx +++ b/packages/design-system/src/components/ds-slider/ds-slider.tsx @@ -3,16 +3,9 @@ import classNames from 'classnames'; import { useControlled } from '../../utils/use-controlled'; import { DsTypography } from '../ds-typography'; import type { DsSliderProps, DsSliderValue } from './ds-slider.types'; +import { toArray } from './ds-slider.utils'; import styles from './ds-slider.module.scss'; -const toArray = (value: DsSliderValue | undefined): number[] | undefined => { - if (value === undefined) { - return undefined; - } - - return Array.isArray(value) ? value : [value]; -}; - /** * @summary Selects a numeric value or a numeric interval by dragging thumbs along a track. * diff --git a/packages/design-system/src/components/ds-slider/ds-slider.types.ts b/packages/design-system/src/components/ds-slider/ds-slider.types.ts index 353288f23..72944b3b9 100644 --- a/packages/design-system/src/components/ds-slider/ds-slider.types.ts +++ b/packages/design-system/src/components/ds-slider/ds-slider.types.ts @@ -37,6 +37,7 @@ export type DsSliderProps = { /** * Drives thumb and track sizing together (S/M/L → 16/20/24px thumb, 2/4/8px track). + * Accepts a responsive value (e.g. `{ md: 'small', lg: 'medium' }`). * @default 'medium' */ size?: DsSliderSize; @@ -55,6 +56,7 @@ export type DsSliderProps = { /** * Granularity that the value snaps to. Must be greater than 0. + * Accepts a responsive value (e.g. `{ md: 5, lg: 1 }`). * @default 1 */ step?: number; @@ -78,6 +80,7 @@ export type DsSliderProps = { /** * Accessible name per thumb. Required for `type="range"` (e.g. `['Minimum', 'Maximum']`). + * Accepts a responsive value (e.g. `{ md: ['Min', 'Max'], lg: ['Minimum', 'Maximum'] }`). */ thumbLabels?: string[]; diff --git a/packages/design-system/src/components/ds-slider/ds-slider.utils.ts b/packages/design-system/src/components/ds-slider/ds-slider.utils.ts new file mode 100644 index 000000000..8689031fa --- /dev/null +++ b/packages/design-system/src/components/ds-slider/ds-slider.utils.ts @@ -0,0 +1,14 @@ +import type { DsSliderValue } from './ds-slider.types'; + +/** + * Normalizes the public value (a number or a `[min, max]` tuple) into the + * `number[]` shape Ark's slider works with. Returns `undefined` when no value + * is provided so controlled/uncontrolled detection stays intact. + */ +export const toArray = (value: DsSliderValue | undefined): number[] | undefined => { + if (value === undefined) { + return undefined; + } + + return Array.isArray(value) ? value : [value]; +}; diff --git a/packages/design-system/src/components/ds-slider/index.ts b/packages/design-system/src/components/ds-slider/index.ts index 340528817..ca2f1a313 100644 --- a/packages/design-system/src/components/ds-slider/index.ts +++ b/packages/design-system/src/components/ds-slider/index.ts @@ -1,2 +1,11 @@ -export { default as DsSlider } from './ds-slider'; -export type { DsSliderProps, DsSliderValue, DsSliderType, DsSliderSize } from './ds-slider.types'; +import type { ComponentProps } from 'react'; + +import { withResponsiveProps } from '../../utils/responsive'; +import DsSliderBase from './ds-slider'; + +export const DsSlider = withResponsiveProps(DsSliderBase, ['size', 'step', 'thumbLabels']); + +DsSlider.displayName = 'DsSlider'; + +export type DsSliderProps = ComponentProps; +export type { DsSliderValue, DsSliderType, DsSliderSize } from './ds-slider.types'; diff --git a/packages/design-system/tests/storybook/docs-snippets.docs.test.ts b/packages/design-system/tests/storybook/docs-snippets.docs.test.ts index b55b48649..8e11248e5 100644 --- a/packages/design-system/tests/storybook/docs-snippets.docs.test.ts +++ b/packages/design-system/tests/storybook/docs-snippets.docs.test.ts @@ -21,6 +21,7 @@ const COMPONENTS = [ 'date-picker', 'date-range-picker', 'dialog', + 'slider', ]; function getComponentSnapshotPath(name: string): string {