Skip to content

Commit 80f860f

Browse files
authored
feat(component): added a new headless slider component (#169)
* feat(component): added a new headless slider component * style(component): move the component to use new Qwik syntax * style(component): merge Shai refactor + change useClientEffect
1 parent 7d1407f commit 80f860f

File tree

8 files changed

+237
-0
lines changed

8 files changed

+237
-0
lines changed

apps/website/src/components/menu/menu.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export const Menu = component$<Props>(({ onClose$ }) => {
3333
{ label: 'Tabs', path: `/docs/${appState.theme.toLowerCase()}/tabs` },
3434
{ label: 'Toggle', path: `/docs/${appState.theme.toLowerCase()}/toggle` },
3535
{ label: 'Tooltip', path: `/docs/${appState.theme.toLowerCase()}/tooltip` },
36+
{ label: 'Slider', path: `/docs/${appState.theme.toLowerCase()}/slider` },
3637
{
3738
label: 'Progress',
3839
path: `/docs/${appState.theme.toLowerCase()}/progress`,
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { component$ } from '@builder.io/qwik';
2+
import { Slider, SliderProgress, SliderThumb } from '@qwik-ui/headless';
3+
4+
export default component$(() => {
5+
return (
6+
<div class="flex flex-col gap-8 mt-4">
7+
<h2>This is the documentation for the Slider</h2>
8+
<div class="flex flex-col gap-8 mt-4">
9+
<div>
10+
<h2>Basic Example</h2>
11+
<Slider
12+
value={70}
13+
max={100}
14+
min={20}
15+
onChange$={(value: number) => {
16+
console.log(value);
17+
}}
18+
>
19+
<SliderProgress />
20+
<SliderThumb />
21+
</Slider>
22+
</div>
23+
</div>
24+
</div>
25+
);
26+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './slider';
2+
export * from './sliderProgress';
3+
export * from './sliderThumb';
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import {
2+
component$,
3+
createContextId,
4+
PropFunction,
5+
Signal,
6+
Slot,
7+
useBrowserVisibleTask$,
8+
useContextProvider,
9+
useSignal,
10+
} from '@builder.io/qwik';
11+
12+
export const getPercentage = (value: number, min = 0, max = 100) => {
13+
return ((value - min) * 100) / (max - min);
14+
};
15+
16+
interface SliderContextService {
17+
value: Signal<number>;
18+
min: Signal<number>;
19+
max: Signal<number>;
20+
positionX: Signal<number | undefined>;
21+
percentage: Signal<number>;
22+
}
23+
24+
export const sliderContext = createContextId<SliderContextService>('slider');
25+
26+
interface SliderProps {
27+
value: number;
28+
min: number;
29+
max: number;
30+
onChange$?: PropFunction<(value: number) => void>;
31+
}
32+
33+
export const Slider = component$(
34+
({ value = 0, min = 0, max = 100, onChange$ }: SliderProps) => {
35+
const rootPositionRef = useSignal<Element>();
36+
const sliderValue = useSignal(value);
37+
const minSignal = useSignal(min);
38+
const maxSignal = useSignal(max);
39+
const positionXSignal = useSignal<number | undefined>();
40+
const percentageSignal = useSignal(getPercentage(value, min, max));
41+
42+
const contextService: SliderContextService = {
43+
value: sliderValue,
44+
min: minSignal,
45+
max: maxSignal,
46+
positionX: positionXSignal,
47+
percentage: percentageSignal,
48+
};
49+
50+
useBrowserVisibleTask$(async ({ track }) => {
51+
track(() => rootPositionRef);
52+
contextService.positionX.value =
53+
rootPositionRef.value?.getBoundingClientRect().x;
54+
});
55+
56+
useBrowserVisibleTask$(async ({ track }) => {
57+
const newValue = track(() => sliderValue.value);
58+
if (onChange$) {
59+
onChange$(newValue);
60+
}
61+
contextService.percentage.value = getPercentage(newValue, min, max);
62+
});
63+
64+
useContextProvider(sliderContext, contextService);
65+
66+
return (
67+
<div
68+
ref={rootPositionRef}
69+
style={{
70+
display: 'inline-block',
71+
position: 'relative',
72+
border: 'solid 1px rgb(178,178,178)',
73+
borderRadius: '4px',
74+
background: 'rgb(239,239,239)',
75+
width: '100px',
76+
height: '6px',
77+
}}
78+
>
79+
<Slot />
80+
</div>
81+
);
82+
}
83+
);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { component$, Slot, useContext } from '@builder.io/qwik';
2+
import { sliderContext } from './slider';
3+
4+
export const SliderProgress = component$(() => {
5+
const contextService = useContext(sliderContext);
6+
7+
return (
8+
<div
9+
style={{
10+
display: 'block',
11+
position: 'absolute',
12+
top: 0,
13+
height: '100%',
14+
left: 0,
15+
width: `${contextService.percentage.value}%`,
16+
background: 'rgb(0,117,255)',
17+
}}
18+
>
19+
<Slot />
20+
</div>
21+
);
22+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import {
2+
$,
3+
component$,
4+
QwikMouseEvent,
5+
Slot,
6+
useContext,
7+
useOnWindow,
8+
useSignal,
9+
} from '@builder.io/qwik';
10+
import { sliderContext } from './slider';
11+
12+
export const clamp = (value: number, min: number, max: number) => {
13+
return Math.min(max, Math.max(value, min));
14+
};
15+
16+
export const SliderThumb = component$(() => {
17+
const mouseDownHappenedSignal = useSignal(false);
18+
const contextService = useContext(sliderContext);
19+
20+
const handleMouseDown = $((e: QwikMouseEvent) => {
21+
if (contextService.positionX.value) {
22+
contextService.value.value = clamp(
23+
e.pageX - contextService.positionX.value,
24+
contextService.min.value,
25+
contextService.max.value
26+
);
27+
}
28+
mouseDownHappenedSignal.value = true;
29+
});
30+
31+
useOnWindow(
32+
'mousemove',
33+
$((e) => {
34+
if (contextService.positionX.value && mouseDownHappenedSignal.value) {
35+
contextService.value.value = clamp(
36+
(e as MouseEvent).pageX - contextService.positionX.value,
37+
contextService.min.value,
38+
contextService.max.value
39+
);
40+
}
41+
})
42+
);
43+
44+
useOnWindow(
45+
'mouseup',
46+
$((e) => {
47+
if (contextService.positionX.value && mouseDownHappenedSignal.value) {
48+
contextService.value.value = clamp(
49+
(e as MouseEvent).pageX - contextService.positionX.value,
50+
contextService.min.value,
51+
contextService.max.value
52+
);
53+
}
54+
mouseDownHappenedSignal.value = false;
55+
})
56+
);
57+
58+
return (
59+
<div
60+
style={{
61+
width: '20px',
62+
height: '20px',
63+
borderRadius: '20px',
64+
background: 'rgba(0, 0, 250)',
65+
position: 'absolute',
66+
transform: 'translateX(-50%) translateY(-50%)',
67+
top: '50%',
68+
left: `${contextService.percentage.value}%`,
69+
}}
70+
onMouseDown$={handleMouseDown}
71+
>
72+
<Slot />
73+
</div>
74+
);
75+
});

packages/headless/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ export * from './components/toast/toast';
1313
export * from './components/toggle/toggle';
1414
export * from './components/tooltip/tooltip';
1515
export * as Select from './components/select/select';
16+
export * from './components/slider';
1617
export * from './components/radio/radio';
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { component$ } from '@builder.io/qwik';
2+
import { Slider, SliderProgress, SliderThumb } from '@qwik-ui/headless';
3+
4+
export default component$(() => {
5+
return (
6+
<div class="flex flex-col gap-8 mt-4">
7+
<h2>This is the documentation for the Slider</h2>
8+
<div class="flex flex-col gap-8 mt-4">
9+
<div>
10+
<h2>Basic Example</h2>
11+
<Slider
12+
value={70}
13+
max={100}
14+
min={20}
15+
onChange$={(value) => {
16+
console.log(value);
17+
}}
18+
>
19+
<SliderProgress />
20+
<SliderThumb />
21+
</Slider>
22+
</div>
23+
</div>
24+
</div>
25+
);
26+
});

0 commit comments

Comments
 (0)