Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clever-carrots-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@drivenets/design-system': minor
---

Add `DsSlider` component
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# DsSlider docs snippets

## Default

### Show code
<DsSlider
defaultValue={36}
label="Input label"
max={100}
min={0}
type="value"
/>

### MCP manifest
const Default = () => <DsSlider label="Input label" type="value" defaultValue={36} min={0} max={100} />;

## Range

### Show code
<DsSlider
defaultValue={[
36,
60
]}
label="Input label"
max={100}
min={0}
thumbLabels={[
'Minimum',
'Maximum'
]}
type="range"
/>

### MCP manifest
const Range = () => <DsSlider
label="Input label"
type="range"
defaultValue={[36, 60]}
min={0}
max={100}
thumbLabels={['Minimum', 'Maximum']} />;

## Small

### Show code
<DsSlider
defaultValue={36}
label="Input label"
size="small"
/>

### MCP manifest
const Small = () => <DsSlider label="Input label" size="small" defaultValue={36} />;

## Large

### Show code
<DsSlider
defaultValue={36}
label="Input label"
size="large"
/>

### MCP manifest
const Large = () => <DsSlider label="Input label" size="large" defaultValue={36} />;

## Disabled

### Show code
<DsSlider
defaultValue={36}
disabled
label="Input label"
/>

### MCP manifest
const Disabled = () => <DsSlider label="Input label" defaultValue={36} disabled />;

## Controlled

### Show code
{
parameters: {
docs: {
source: {
type: 'code'
}
}
},
render: function Render() {
const [value, setValue] = useState<DsSliderValue>(40);
return <DsSlider label="Volume" value={value} onValueChange={setValue} formatValue={current => `${String(current)}%`} />;
}
}

### MCP manifest
const Controlled = function Render() {
const [value, setValue] = useState<DsSliderValue>(40);

return (
<DsSlider
label="Volume"
value={value}
onValueChange={setValue}
formatValue={(current) => `${String(current)}%`}
/>
);
};
Original file line number Diff line number Diff line change
@@ -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<typeof page.getByRole>) {
(locator.element() as HTMLElement).focus();
}

describe('DsSlider', () => {
it('renders the label and the numeric output', async () => {
await page.render(<DsSlider label="Input label" defaultValue={36} />);

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(<DsSlider label="Level" defaultValue={36} onValueChange={onValueChange} />);

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(
<DsSlider label="Interval" type="range" defaultValue={[36, 60]} thumbLabels={['Minimum', 'Maximum']} />,
);

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(<DsSlider label="Level" defaultValue={36} disabled onValueChange={onValueChange} />);

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<DsSliderValue>(20);

return <DsSlider label="Volume" value={value} onValueChange={setValue} />;
}

await page.render(<Controlled />);

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(
<DsSlider label="Volume" defaultValue={40} formatValue={(current) => `${String(current)}%`} />,
);

await expect.element(page.getByText('40%')).toBeVisible();
});
});
29 changes: 29 additions & 0 deletions packages/design-system/src/components/ds-slider/ds-slider.figma.ts
Original file line number Diff line number Diff line change
@@ -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`<DsSlider ${attrs} />`,
imports: ["import { DsSlider } from '@drivenets/design-system';"],
id: 'ds-slider',
metadata: { nestable: false },
} satisfies figma.Template;
104 changes: 104 additions & 0 deletions packages/design-system/src/components/ds-slider/ds-slider.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
.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;
--ds-slider-radius: 9999px;
--ds-slider-transition: 0.2s;

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: var(--ds-slider-radius);
}

.range {
height: 100%;
background: var(--background-primary);
border-radius: var(--ds-slider-radius);
}

.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: var(--ds-slider-radius);
outline: none;
cursor: pointer;
transition:
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 {
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);
}
}
Loading
Loading