Skip to content

Commit 1a13435

Browse files
authored
MN-459: Created Slider component (#674)
* Created Slider component * fixed PR * Added eslint-naming-convention for interface/type/enum + fix TSDoc/Doc
1 parent ee8484c commit 1a13435

File tree

12 files changed

+567
-3
lines changed

12 files changed

+567
-3
lines changed

apps/monk-test-app/src/views/CameraView/CameraView.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {
22
Camera,
3-
CameraFacingMode,
43
CameraResolution,
54
CompressionFormat,
65
MonkPicture,
@@ -12,7 +11,6 @@ import './CameraView.css';
1211

1312
export function CameraView() {
1413
const [state] = useState({
15-
facingMode: CameraFacingMode.ENVIRONMENT,
1614
resolution: CameraResolution.UHD_4K,
1715
compressionFormat: CompressionFormat.JPEG,
1816
quality: '0.8',
@@ -27,7 +25,6 @@ export function CameraView() {
2725
<div className='camera-view-container'>
2826
<Camera
2927
HUDComponent={SimpleCameraHUD}
30-
facingMode={state.facingMode}
3128
resolution={state.resolution}
3229
format={state.compressionFormat}
3330
quality={Number(state.quality)}

packages/private/eslint-config-typescript-react/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ module.exports = {
1616
'jsx-a11y/media-has-caption': OFF,
1717
'jsx-a11y/anchor-has-content': OFF,
1818
'jsx-a11y/anchor-is-valid': OFF,
19+
'jsx-a11y/no-noninteractive-element-interactions': OFF,
1920
},
2021
ignorePatterns: ['**/*.js', 'node_modules', 'dist'],
2122
overrides: [

packages/private/eslint-config-typescript/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ module.exports = {
3333
}
3434
],
3535
'no-underscore-dangle': OFF,
36+
'@typescript-eslint/naming-convention': [
37+
ERROR,
38+
{ selector: 'interface', format: ['PascalCase'] },
39+
{ selector: 'enum', format: ['PascalCase'] },
40+
{ selector: 'typeAlias', format: ['PascalCase'] },
41+
],
3642
},
3743
overrides: [
3844
{

packages/public/common-ui-web/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,39 @@ function App() {
222222

223223
---
224224

225+
## Slider
226+
### Description
227+
Slider component that can be used to select a value within a specified range by dragging along a horizontal track.
228+
229+
### Examples
230+
231+
```tsx
232+
import { useState } from 'react';
233+
import { Slider } from '@monkvision/common-ui-web';
234+
235+
function App() {
236+
const [value, setValue] = useState(0);
237+
const handleChange = (newValue: number) => {setValue(newValue)}
238+
239+
return <Slider value={value} min={0} max={1000} step={20} onChange={handleChange}/>;
240+
}
241+
```
242+
### Props
243+
| Prop | Type | Description | Required | Default Value |
244+
|----------------|---------------------------|--------------------------------------------------------------------------------------------------------------|----------|----------------------|
245+
| min | number | The minimum value of the slider. | | `0` |
246+
| max | number | The maximum value of the slider. | | `100` |
247+
| value | number | The current value of the slider. | | `(max - min) / 2` |
248+
| primaryColor | ColorProp | The name or hexcode used for the thumb/knob border. | | `'primary'` |
249+
| secondaryColor | ColorProp | The name or hexcode used for the progress bar. | | `'primary'` |
250+
| tertiaryColor | ColorProp | The name or hexcode used for the track bar background. | | `'secondary-xlight'` |
251+
| disabled | boolean | Boolean indicating if the slider is disabled or not. | | `false` |
252+
| step | number | The increment value of the slider. | | `1` |
253+
| onChange | `(value: number) => void` | Callback function invoked when the slider value changes. | | |
254+
| style | CSSProperties | This property allows custom CSS styles for the slider. `width` sets slider width but `height` has no effect. | | |
255+
256+
---
257+
225258
## Spinner
226259
### Description
227260
A simple spinner component that displays a loading spinner.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Styles } from '@monkvision/types';
2+
3+
export const styles: Styles = {
4+
sliderStyle: {
5+
position: 'relative',
6+
display: 'flex',
7+
width: '129px',
8+
height: '25px',
9+
background: 'transparent',
10+
cursor: 'pointer',
11+
margin: '20px',
12+
alignItems: 'center',
13+
},
14+
trackBarStyle: {
15+
position: 'absolute',
16+
width: '100%',
17+
height: '3px',
18+
borderRadius: '5px',
19+
},
20+
thumbStyle: {
21+
position: 'absolute',
22+
top: '50%',
23+
transform: 'translate(-50%, -50%)',
24+
background: 'white',
25+
width: '22px',
26+
height: '22px',
27+
borderRadius: '50%',
28+
border: 'solid 3px',
29+
},
30+
thumbSmall: {
31+
width: '11px',
32+
height: '11px',
33+
},
34+
progressBarStyle: {
35+
position: 'absolute',
36+
height: '3px',
37+
borderRadius: '5px',
38+
},
39+
sliderDisabled: {
40+
opacity: 0.37,
41+
cursor: 'default',
42+
},
43+
hoverStyle: {
44+
background: 'transparent',
45+
border: 'none',
46+
width: '25px',
47+
height: '25px',
48+
},
49+
hovered: {
50+
border: 'solid 15px',
51+
opacity: '15%',
52+
},
53+
};
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { useRef } from 'react';
2+
import { useInteractiveStatus } from '@monkvision/common';
3+
import { SliderProps, useSliderStyle, useSlider } from './hooks';
4+
5+
/**
6+
* A Slider component that allows users to select a value within a specified range by dragging along a horizontal track.
7+
*/
8+
export function Slider({
9+
min = 0,
10+
max = 100,
11+
value = (max - min) / 2,
12+
primaryColor = 'primary',
13+
secondaryColor = 'primary',
14+
tertiaryColor = 'secondary-xlight',
15+
disabled = false,
16+
step = 1,
17+
onChange,
18+
style,
19+
}: SliderProps) {
20+
const sliderRef = useRef<HTMLDivElement>(null);
21+
const { thumbPosition, handleStart, isDragging } = useSlider({
22+
sliderRef,
23+
value,
24+
min,
25+
max,
26+
step: step > 0 ? step : 1,
27+
disabled,
28+
onChange,
29+
});
30+
const { status, eventHandlers } = useInteractiveStatus({
31+
disabled,
32+
});
33+
const { sliderStyle, thumbStyle, progressBarStyle, trackBarStyle, hoverThumbStyle } =
34+
useSliderStyle({
35+
primaryColor,
36+
secondaryColor,
37+
tertiaryColor,
38+
style,
39+
status,
40+
});
41+
42+
return (
43+
<div
44+
role='button'
45+
tabIndex={0}
46+
ref={sliderRef}
47+
style={sliderStyle}
48+
onMouseDown={handleStart}
49+
onTouchStart={handleStart}
50+
data-testid='slider'
51+
>
52+
<div style={{ ...trackBarStyle }} data-testid='track' />
53+
<div style={{ ...progressBarStyle, width: `${thumbPosition}%` }} data-testid='value-track' />
54+
<div style={{ ...hoverThumbStyle, left: `${thumbPosition}%` }} data-testid='hover-thumb' />
55+
<div
56+
style={{
57+
cursor: isDragging ? 'grabbing' : 'grab',
58+
...thumbStyle,
59+
left: `${thumbPosition}%`,
60+
}}
61+
onMouseEnter={eventHandlers.onMouseEnter}
62+
onMouseLeave={eventHandlers.onMouseLeave}
63+
data-testid='thumb'
64+
/>
65+
</div>
66+
);
67+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './useSlider';
2+
export * from './useSliderStyle';
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { useState, RefObject, useLayoutEffect } from 'react';
2+
3+
function getFirstThumbPosition(min: number, max: number, value: number): number {
4+
if (value > max) {
5+
return 100;
6+
}
7+
if (value < min) {
8+
return 0;
9+
}
10+
return ((value - min) / (max - min)) * 100;
11+
}
12+
13+
function getThumbPosition(
14+
event: MouseEvent | TouchEvent,
15+
sliderRef: RefObject<HTMLDivElement>,
16+
step: number,
17+
min: number,
18+
max: number,
19+
): number {
20+
if (!sliderRef.current) {
21+
return 0;
22+
}
23+
const sliderElement = sliderRef.current;
24+
const sliderRect = sliderElement.getBoundingClientRect();
25+
const offsetX = Math.max(
26+
0,
27+
Math.min(
28+
sliderRect.width,
29+
(event instanceof MouseEvent ? event.clientX : event.touches[0].clientX) - sliderRect.left,
30+
),
31+
);
32+
const positionPercentage = (offsetX / sliderRect.width) * 100;
33+
const stepPercentage = 100 / ((max - min) / step);
34+
return Math.round(positionPercentage / stepPercentage) * stepPercentage;
35+
}
36+
37+
function getNewSliderValue(
38+
roundedPercentage: number,
39+
max: number,
40+
min: number,
41+
step: number,
42+
): number {
43+
let multiplier = 1;
44+
if (!Number.isInteger(step)) {
45+
const nbDigitAfterDot = step.toString().split('.')[1].length;
46+
multiplier = 10 ** nbDigitAfterDot;
47+
}
48+
return Math.round(((max - min) * (roundedPercentage / 100) + min) * multiplier) / multiplier;
49+
}
50+
51+
export interface UseSliderParams {
52+
sliderRef: RefObject<HTMLDivElement>;
53+
value: number;
54+
min: number;
55+
max: number;
56+
step: number;
57+
disabled: boolean;
58+
onChange?: (value: number) => void;
59+
}
60+
61+
export function useSlider({
62+
sliderRef,
63+
value,
64+
min,
65+
max,
66+
step,
67+
disabled,
68+
onChange,
69+
}: UseSliderParams) {
70+
const [thumbPosition, setThumbPosition] = useState(getFirstThumbPosition(min, max, value));
71+
const [isDragging, setIsDragging] = useState(false);
72+
73+
const handleStart = () => {
74+
setIsDragging(true);
75+
};
76+
77+
const handleEnd = () => {
78+
setIsDragging(false);
79+
};
80+
81+
const handleMove = (event: MouseEvent | TouchEvent) => {
82+
event.preventDefault();
83+
if (!disabled && max > min && onChange) {
84+
const roundedPercentage = getThumbPosition(event, sliderRef, step, min, max);
85+
setThumbPosition(roundedPercentage);
86+
87+
const newValue = getNewSliderValue(roundedPercentage, max, min, step);
88+
onChange(newValue);
89+
}
90+
};
91+
92+
useLayoutEffect(() => {
93+
if (!isDragging) {
94+
return () => {};
95+
}
96+
document.addEventListener('mousemove', handleMove);
97+
document.addEventListener('touchmove', handleMove, { passive: false });
98+
99+
return () => {
100+
document.removeEventListener('mousemove', handleMove);
101+
document.removeEventListener('touchmove', handleMove);
102+
};
103+
}, [isDragging]);
104+
105+
useLayoutEffect(() => {
106+
document.addEventListener('mouseup', handleEnd);
107+
document.addEventListener('touchend', handleEnd);
108+
sliderRef?.current?.addEventListener('click', handleMove);
109+
110+
return () => {
111+
document.removeEventListener('mouseup', handleEnd);
112+
document.removeEventListener('touchend', handleEnd);
113+
sliderRef?.current?.removeEventListener('click', handleMove);
114+
};
115+
}, []);
116+
117+
return { thumbPosition, handleStart, isDragging };
118+
}

0 commit comments

Comments
 (0)