Skip to content

Commit 5f01946

Browse files
authored
feat(RangeSlider): range slider with custom theme, storybook, unit test and docs (#548)
feat(RangeSlider): range slider with custom theme, stroybook, unit test and docs
1 parent a2d3f74 commit 5f01946

File tree

9 files changed

+413
-0
lines changed

9 files changed

+413
-0
lines changed

src/docs/pages/FormsPage.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Checkbox } from '../../lib/components/Checkbox';
66
import { FileInput } from '../../lib/components/FileInput';
77
import { Label } from '../../lib/components/Label';
88
import { Radio } from '../../lib/components/Radio';
9+
import { RangeSlider } from '../../lib/components/RangeSlider';
910
import { Select } from '../../lib/components/Select';
1011
import { Textarea } from '../../lib/components/Textarea';
1112
import { TextInput } from '../../lib/components/TextInput';
@@ -387,6 +388,43 @@ const FormsPage: FC = () => {
387388
</div>
388389
),
389390
},
391+
{
392+
title: 'Range Slider',
393+
code: (
394+
<div className="flex flex-col gap-4">
395+
<div>
396+
<div className="mb-1 block">
397+
<Label htmlFor="default-range" value="Default" />
398+
</div>
399+
<RangeSlider id="default-range" />
400+
</div>
401+
<div>
402+
<div className="mb-1 block">
403+
<Label htmlFor="disbaled-range" value="Disabled" />
404+
</div>
405+
<RangeSlider id="disabled-range" disabled={true} />
406+
</div>
407+
<div>
408+
<div className="mb-1 block">
409+
<Label htmlFor="sm-range" value="Small" />
410+
</div>
411+
<RangeSlider id="sm-range" sizing="sm" />
412+
</div>
413+
<div>
414+
<div className="mb-1 block">
415+
<Label htmlFor="md-range" value="Medium" />
416+
</div>
417+
<RangeSlider id="md-range" sizing="md" />
418+
</div>
419+
<div>
420+
<div className="mb-1 block">
421+
<Label htmlFor="lg-range" value="Large" />
422+
</div>
423+
<RangeSlider id="lg-range" sizing="lg" />
424+
</div>
425+
</div>
426+
),
427+
},
390428
];
391429

392430
return <DemoPage examples={examples} />;

src/lib/components/Flowbite/FlowbiteTheme.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { FlowbiteNavbarTheme } from '../Navbar';
2020
import { FlowbitePaginationTheme } from '../Pagination';
2121
import { FlowbiteProgressTheme } from '../Progress';
2222
import { FlowbiteRadioTheme } from '../Radio';
23+
import { FlowbiteRangeSliderTheme } from '../RangeSlider';
2324
import { FlowbiteRatingTheme } from '../Rating';
2425
import { FlowbiteSelectTheme } from '../Select';
2526
import { FlowbiteSidebarTheme } from '../Sidebar';
@@ -65,6 +66,7 @@ export interface FlowbiteTheme extends Record<string, unknown> {
6566
fileInput: FlowbiteFileInputTheme;
6667
label: FlowbiteLabelTheme;
6768
radio: FlowbiteRadioTheme;
69+
rangeSlider: FlowbiteRangeSliderTheme;
6870
select: FlowbiteSelectTheme;
6971
textInput: FlowbiteTextInputTheme;
7072
textarea: FlowbiteTextareaTheme;

src/lib/components/Label/Label.spec.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Button } from '../Button';
55
import { Checkbox } from '../Checkbox';
66
import { FileInput } from '../FileInput';
77
import { Radio } from '../Radio';
8+
import { RangeSlider } from '../RangeSlider';
89
import { Select } from '../Select';
910
import { Textarea } from '../Textarea';
1011
import { TextInput } from '../TextInput';
@@ -22,6 +23,7 @@ describe.concurrent('Components / Label', () => {
2223
'Upload file',
2324
'United States',
2425
'Your message',
26+
'Price',
2527
];
2628

2729
const { getByLabelText } = render(<TestForm />);
@@ -98,6 +100,10 @@ const TestForm = (): JSX.Element => (
98100
<Label htmlFor="comment">Your message</Label>
99101
<Textarea id="comment" helperText="Leave a comment..." required rows={4} />
100102
</div>
103+
<div>
104+
<Label htmlFor="price">Price</Label>
105+
<RangeSlider id="price" min={0} max={100} />
106+
</div>
101107
</fieldset>
102108
<Button type="submit">Submit</Button>
103109
</form>
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import { render, screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { createRef } from 'react';
4+
import { describe, expect, it, vi } from 'vitest';
5+
6+
import { Flowbite } from '../Flowbite';
7+
import { RangeSlider } from './RangeSlider';
8+
9+
describe('Components / Button', () => {
10+
describe('A11y', () => {
11+
it('should have `role="progressbar"` by default', () => {
12+
render(<RangeSlider />);
13+
14+
expect(rangeSlider()).toBeInTheDocument();
15+
});
16+
17+
it('should be able to use any other role permitted for `RangeSlider`', () => {
18+
render(<RangeSlider role="rangeinput" />);
19+
20+
expect(rangeSlider('rangeinput')).toBeInTheDocument();
21+
});
22+
});
23+
24+
describe('Keyboard interactions', () => {
25+
it('should focus when `Tab` is pressed', async () => {
26+
const user = userEvent.setup();
27+
render(<RangeSlider />);
28+
29+
await user.tab();
30+
31+
expect(rangeSlider()).toHaveFocus();
32+
});
33+
34+
it('should be possible to `Tab` out', async () => {
35+
const user = userEvent.setup();
36+
render(
37+
<>
38+
<RangeSlider />
39+
<RangeSlider />
40+
<RangeSlider />
41+
</>,
42+
);
43+
44+
const rangeSliderElements = rangeSliders();
45+
46+
await user.tab();
47+
48+
expect(rangeSliderElements[0]).toHaveFocus();
49+
50+
await user.tab();
51+
52+
expect(rangeSliderElements[1]).toHaveFocus();
53+
54+
await user.tab();
55+
56+
expect(rangeSliderElements[2]).toHaveFocus();
57+
});
58+
59+
it('should not trigger `onChange` when `Space` is pressed', async () => {
60+
const user = userEvent.setup();
61+
const handleChange = vi.fn();
62+
63+
render(<RangeSlider onChange={handleChange} />);
64+
65+
await user.tab();
66+
67+
expect(rangeSlider()).toHaveFocus();
68+
69+
await user.keyboard('[Space]');
70+
71+
expect(handleChange).not.toHaveBeenCalled();
72+
});
73+
74+
it('should not trigger `onChange` when `Enter` is pressed', async () => {
75+
const user = userEvent.setup();
76+
const handleChange = vi.fn();
77+
78+
render(<RangeSlider onChange={handleChange} />);
79+
80+
await user.tab();
81+
82+
expect(rangeSlider()).toHaveFocus();
83+
84+
await user.keyboard('[Enter]');
85+
86+
expect(handleChange).not.toHaveBeenCalled();
87+
});
88+
89+
/**
90+
* Test Name: Should trigger `onChange` when `Arrow` key pressed
91+
*
92+
* This test is not testable because there is no support for
93+
* input[type="range"] in the user-event library.
94+
*
95+
* Issues:
96+
* https://github.com/testing-library/user-event/issues/1067
97+
* https://github.com/testing-library/user-event/issues/871
98+
*
99+
* TODO: Once these issues get fixed, we will add this test case.
100+
*
101+
*/
102+
});
103+
104+
describe('Props', () => {
105+
it('should allow HTML attributes for `<input type="range">`s', () => {
106+
render(<RangeSlider formAction="post.php" min={4} max={10} step={0.5} />);
107+
const rangeSliderElement = rangeSlider();
108+
expect(rangeSliderElement).toHaveAttribute('formAction', 'post.php');
109+
expect(rangeSliderElement).toHaveAttribute('min', '4');
110+
expect(rangeSliderElement).toHaveAttribute('max', '10');
111+
expect(rangeSliderElement).toHaveAttribute('step', '0.5');
112+
});
113+
114+
it('should be disabled when `disabled={true}`', () => {
115+
render(<RangeSlider disabled />);
116+
117+
expect(rangeSlider()).toBeDisabled();
118+
});
119+
120+
it('should be required when `required={true}`', () => {
121+
render(<RangeSlider required={true} />);
122+
expect(rangeSlider()).toHaveAttribute('required');
123+
});
124+
125+
it('should allow ref as prop', () => {
126+
const ref = createRef<HTMLInputElement>();
127+
render(<RangeSlider ref={ref} name="range_slider_name" />);
128+
expect(ref.current?.name).toBe('range_slider_name');
129+
});
130+
});
131+
132+
describe('Rendering', () => {
133+
it('should render with no props', () => {
134+
render(<RangeSlider />);
135+
expect(rangeSlider()).toBeInTheDocument();
136+
});
137+
});
138+
139+
describe('Theme', () => {
140+
it('should use `base` classes', () => {
141+
const theme = {
142+
rangeSlider: {
143+
base: 'dummy-range-slider-base-classes',
144+
},
145+
};
146+
147+
render(
148+
<Flowbite theme={{ theme }}>
149+
<RangeSlider />
150+
</Flowbite>,
151+
);
152+
153+
expect(rangeSliderContainer()).toHaveClass('dummy-range-slider-base-classes');
154+
});
155+
156+
it('should use `base` classes of field', () => {
157+
const theme = {
158+
rangeSlider: {
159+
field: {
160+
base: 'dummy-range-slider-field-base-classes',
161+
},
162+
},
163+
};
164+
165+
render(
166+
<Flowbite theme={{ theme }}>
167+
<RangeSlider />
168+
</Flowbite>,
169+
);
170+
171+
expect(rangeSliderContainer().childNodes[0]).toHaveClass('dummy-range-slider-field-base-classes');
172+
});
173+
174+
it('should use `base` classes of input', () => {
175+
const theme = {
176+
rangeSlider: {
177+
field: {
178+
input: {
179+
base: 'dummy-range-slider-field-input-base-classes',
180+
},
181+
},
182+
},
183+
};
184+
185+
render(
186+
<Flowbite theme={{ theme }}>
187+
<RangeSlider />
188+
</Flowbite>,
189+
);
190+
191+
expect(rangeSlider()).toHaveClass('dummy-range-slider-field-input-base-classes');
192+
});
193+
194+
it('should use `sizes` classes of input', () => {
195+
const theme = {
196+
rangeSlider: {
197+
field: {
198+
input: {
199+
sizes: {
200+
lg: 'dummy-range-slider-field-input-sizes-lg-classes',
201+
},
202+
},
203+
},
204+
},
205+
};
206+
207+
render(
208+
<Flowbite theme={{ theme }}>
209+
<RangeSlider sizing="lg" />
210+
</Flowbite>,
211+
);
212+
213+
expect(rangeSlider()).toHaveClass('dummy-range-slider-field-input-sizes-lg-classes');
214+
});
215+
});
216+
217+
describe('Theme as a prop', () => {
218+
it('should use `base` classes', () => {
219+
const theme = {
220+
base: 'dummy-range-slider-base-classes',
221+
};
222+
223+
render(<RangeSlider theme={theme} />);
224+
225+
expect(rangeSliderContainer()).toHaveClass('dummy-range-slider-base-classes');
226+
});
227+
228+
it('should use `base` classes of field', () => {
229+
const theme = {
230+
field: {
231+
base: 'dummy-range-slider-field-base-classes',
232+
},
233+
};
234+
235+
render(<RangeSlider theme={theme} />);
236+
237+
expect(rangeSliderContainer().childNodes[0]).toHaveClass('dummy-range-slider-field-base-classes');
238+
});
239+
240+
it('should use `base` classes of input', () => {
241+
const theme = {
242+
field: {
243+
input: {
244+
base: 'dummy-range-slider-field-input-base-classes',
245+
},
246+
},
247+
};
248+
249+
render(<RangeSlider theme={theme} />);
250+
251+
expect(rangeSlider()).toHaveClass('dummy-range-slider-field-input-base-classes');
252+
});
253+
254+
it('should use `sizes` classes of input', () => {
255+
const theme = {
256+
field: {
257+
input: {
258+
sizes: {
259+
lg: 'dummy-range-slider-field-input-sizes-lg-classes',
260+
},
261+
},
262+
},
263+
};
264+
265+
render(<RangeSlider sizing="lg" theme={theme} />);
266+
267+
expect(rangeSlider()).toHaveClass('dummy-range-slider-field-input-sizes-lg-classes');
268+
});
269+
});
270+
});
271+
272+
const rangeSliderContainer = () => screen.getByTestId('flowbite-range-slider');
273+
const rangeSlider = (role = 'slider') => screen.getByRole(role);
274+
const rangeSliders = (role = 'slider') => screen.getAllByRole(role);

0 commit comments

Comments
 (0)