Skip to content
Merged
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
3 changes: 0 additions & 3 deletions apps/monk-test-app/src/views/CameraView/CameraView.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
Camera,
CameraFacingMode,
CameraResolution,
CompressionFormat,
MonkPicture,
Expand All @@ -12,7 +11,6 @@ import './CameraView.css';

export function CameraView() {
const [state] = useState({
facingMode: CameraFacingMode.ENVIRONMENT,
resolution: CameraResolution.UHD_4K,
compressionFormat: CompressionFormat.JPEG,
quality: '0.8',
Expand All @@ -27,7 +25,6 @@ export function CameraView() {
<div className='camera-view-container'>
<Camera
HUDComponent={SimpleCameraHUD}
facingMode={state.facingMode}
resolution={state.resolution}
format={state.compressionFormat}
quality={Number(state.quality)}
Expand Down
1 change: 1 addition & 0 deletions packages/private/eslint-config-typescript-react/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module.exports = {
'jsx-a11y/media-has-caption': OFF,
'jsx-a11y/anchor-has-content': OFF,
'jsx-a11y/anchor-is-valid': OFF,
'jsx-a11y/no-noninteractive-element-interactions': OFF,
},
ignorePatterns: ['**/*.js', 'node_modules', 'dist'],
overrides: [
Expand Down
6 changes: 6 additions & 0 deletions packages/private/eslint-config-typescript/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ module.exports = {
}
],
'no-underscore-dangle': OFF,
'@typescript-eslint/naming-convention': [
ERROR,
{ selector: 'interface', format: ['PascalCase'] },
{ selector: 'enum', format: ['PascalCase'] },
{ selector: 'typeAlias', format: ['PascalCase'] },
],
},
overrides: [
{
Expand Down
33 changes: 33 additions & 0 deletions packages/public/common-ui-web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,39 @@ function App() {

---

## Slider
### Description
Slider component that can be used to select a value within a specified range by dragging along a horizontal track.

### Examples

```tsx
import { useState } from 'react';
import { Slider } from '@monkvision/common-ui-web';

function App() {
const [value, setValue] = useState(0);
const handleChange = (newValue: number) => {setValue(newValue)}

return <Slider value={value} min={0} max={1000} step={20} onChange={handleChange}/>;
}
```
### Props
| Prop | Type | Description | Required | Default Value |
|----------------|---------------------------|--------------------------------------------------------------------------------------------------------------|----------|----------------------|
| min | number | The minimum value of the slider. | | `0` |
| max | number | The maximum value of the slider. | | `100` |
| value | number | The current value of the slider. | | `(max - min) / 2` |
| primaryColor | ColorProp | The name or hexcode used for the thumb/knob border. | | `'primary'` |
| secondaryColor | ColorProp | The name or hexcode used for the progress bar. | | `'primary'` |
| tertiaryColor | ColorProp | The name or hexcode used for the track bar background. | | `'secondary-xlight'` |
| disabled | boolean | Boolean indicating if the slider is disabled or not. | | `false` |
| step | number | The increment value of the slider. | | `1` |
| onChange | `(value: number) => void` | Callback function invoked when the slider value changes. | | |
| style | CSSProperties | This property allows custom CSS styles for the slider. `width` sets slider width but `height` has no effect. | | |

---

## Spinner
### Description
A simple spinner component that displays a loading spinner.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Styles } from '@monkvision/types';

export const styles: Styles = {
sliderStyle: {
position: 'relative',
display: 'flex',
width: '129px',
height: '25px',
background: 'transparent',
cursor: 'pointer',
margin: '20px',
alignItems: 'center',
},
trackBarStyle: {
position: 'absolute',
width: '100%',
height: '3px',
borderRadius: '5px',
},
thumbStyle: {
position: 'absolute',
top: '50%',
transform: 'translate(-50%, -50%)',
background: 'white',
width: '22px',
height: '22px',
borderRadius: '50%',
border: 'solid 3px',
},
thumbSmall: {
width: '11px',
height: '11px',
},
progressBarStyle: {
position: 'absolute',
height: '3px',
borderRadius: '5px',
},
sliderDisabled: {
opacity: 0.37,
cursor: 'default',
},
hoverStyle: {
background: 'transparent',
border: 'none',
width: '25px',
height: '25px',
},
hovered: {
border: 'solid 15px',
opacity: '15%',
},
};
67 changes: 67 additions & 0 deletions packages/public/common-ui-web/src/components/Slider/Slider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useRef } from 'react';
import { useInteractiveStatus } from '@monkvision/common';
import { SliderProps, useSliderStyle, useSlider } from './hooks';

/**
* A Slider component that allows users to select a value within a specified range by dragging along a horizontal track.
*/
export function Slider({
min = 0,
max = 100,
value = (max - min) / 2,
primaryColor = 'primary',
secondaryColor = 'primary',
tertiaryColor = 'secondary-xlight',
disabled = false,
step = 1,
onChange,
style,
}: SliderProps) {
const sliderRef = useRef<HTMLDivElement>(null);
const { thumbPosition, handleStart, isDragging } = useSlider({
sliderRef,
value,
min,
max,
step: step > 0 ? step : 1,
disabled,
onChange,
});
const { status, eventHandlers } = useInteractiveStatus({
disabled,
});
const { sliderStyle, thumbStyle, progressBarStyle, trackBarStyle, hoverThumbStyle } =
useSliderStyle({
primaryColor,
secondaryColor,
tertiaryColor,
style,
status,
});

return (
<div
role='button'
tabIndex={0}
ref={sliderRef}
style={sliderStyle}
onMouseDown={handleStart}
onTouchStart={handleStart}
data-testid='slider'
>
<div style={{ ...trackBarStyle }} data-testid='track' />
<div style={{ ...progressBarStyle, width: `${thumbPosition}%` }} data-testid='value-track' />
<div style={{ ...hoverThumbStyle, left: `${thumbPosition}%` }} data-testid='hover-thumb' />
<div
style={{
cursor: isDragging ? 'grabbing' : 'grab',
...thumbStyle,
left: `${thumbPosition}%`,
}}
onMouseEnter={eventHandlers.onMouseEnter}
onMouseLeave={eventHandlers.onMouseLeave}
data-testid='thumb'
/>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './useSlider';
export * from './useSliderStyle';
118 changes: 118 additions & 0 deletions packages/public/common-ui-web/src/components/Slider/hooks/useSlider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { useState, RefObject, useLayoutEffect } from 'react';

function getFirstThumbPosition(min: number, max: number, value: number): number {
if (value > max) {
return 100;
}
if (value < min) {
return 0;
}
return ((value - min) / (max - min)) * 100;
}

function getThumbPosition(
event: MouseEvent | TouchEvent,
sliderRef: RefObject<HTMLDivElement>,
step: number,
min: number,
max: number,
): number {
if (!sliderRef.current) {
return 0;
}
const sliderElement = sliderRef.current;
const sliderRect = sliderElement.getBoundingClientRect();
const offsetX = Math.max(
0,
Math.min(
sliderRect.width,
(event instanceof MouseEvent ? event.clientX : event.touches[0].clientX) - sliderRect.left,
),
);
const positionPercentage = (offsetX / sliderRect.width) * 100;
const stepPercentage = 100 / ((max - min) / step);
return Math.round(positionPercentage / stepPercentage) * stepPercentage;
}

function getNewSliderValue(
roundedPercentage: number,
max: number,
min: number,
step: number,
): number {
let multiplier = 1;
if (!Number.isInteger(step)) {
const nbDigitAfterDot = step.toString().split('.')[1].length;
multiplier = 10 ** nbDigitAfterDot;
}
return Math.round(((max - min) * (roundedPercentage / 100) + min) * multiplier) / multiplier;
}

export interface UseSliderParams {
sliderRef: RefObject<HTMLDivElement>;
value: number;
min: number;
max: number;
step: number;
disabled: boolean;
onChange?: (value: number) => void;
}

export function useSlider({
sliderRef,
value,
min,
max,
step,
disabled,
onChange,
}: UseSliderParams) {
const [thumbPosition, setThumbPosition] = useState(getFirstThumbPosition(min, max, value));
const [isDragging, setIsDragging] = useState(false);

const handleStart = () => {
setIsDragging(true);
};

const handleEnd = () => {
setIsDragging(false);
};

const handleMove = (event: MouseEvent | TouchEvent) => {
event.preventDefault();
if (!disabled && max > min && onChange) {
const roundedPercentage = getThumbPosition(event, sliderRef, step, min, max);
setThumbPosition(roundedPercentage);

const newValue = getNewSliderValue(roundedPercentage, max, min, step);
onChange(newValue);
}
};

useLayoutEffect(() => {
if (!isDragging) {
return () => {};
}
document.addEventListener('mousemove', handleMove);
document.addEventListener('touchmove', handleMove, { passive: false });

return () => {
document.removeEventListener('mousemove', handleMove);
document.removeEventListener('touchmove', handleMove);
};
}, [isDragging]);

useLayoutEffect(() => {
document.addEventListener('mouseup', handleEnd);
document.addEventListener('touchend', handleEnd);
sliderRef?.current?.addEventListener('click', handleMove);

return () => {
document.removeEventListener('mouseup', handleEnd);
document.removeEventListener('touchend', handleEnd);
sliderRef?.current?.removeEventListener('click', handleMove);
};
}, []);

return { thumbPosition, handleStart, isDragging };
}
Loading