aboutsummaryrefslogtreecommitdiff
path: root/packages/components/src/components/Slider/Slider.tsx
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2025-03-10 16:13:04 -0400
committerbobzel <zzzman@gmail.com>2025-03-10 16:13:04 -0400
commitb7989dded8bb001876de6cbca59bf77935f0daf7 (patch)
tree0dba0665674db7bb84770833df0a4100d0520701 /packages/components/src/components/Slider/Slider.tsx
parent4979415d4604d280e81a162bf9a9d39c731d3738 (diff)
parent5bf944035c0ba94ad15245416f51ca0329a51bde (diff)
Merge branch 'master' into alyssa-starter
Diffstat (limited to 'packages/components/src/components/Slider/Slider.tsx')
-rw-r--r--packages/components/src/components/Slider/Slider.tsx188
1 files changed, 188 insertions, 0 deletions
diff --git a/packages/components/src/components/Slider/Slider.tsx b/packages/components/src/components/Slider/Slider.tsx
new file mode 100644
index 000000000..5af945383
--- /dev/null
+++ b/packages/components/src/components/Slider/Slider.tsx
@@ -0,0 +1,188 @@
+import React, { useState } from 'react';
+import { Colors, getFontSize, getHeight, Size, getFormLabelSize, isDark, INumberProps } from '../../global';
+import './Slider.scss';
+
+export interface ISliderProps extends INumberProps {
+ multithumb: boolean;
+ autorangeMinVal?: number; // minimimum value that min can have when autoranging
+ autorangeMinSize?: number; // minimum difference between min and max when autoranging
+ autorange?: number; // automatically adjust min/max to be +/- autorange/2 around the current value when the thumb is 15% from the min/max, or when the multithumbs are within 20% of the range and the range is bigger than autorange
+ endNumber?: number;
+ setEndNumber?: (newVal: number) => void;
+ setFinalNumber?: (newVal: number) => void;
+ setFinalEndNumber?: (newVal: number) => void;
+ decimals?: number;
+ step?: number;
+ minDiff?: number;
+}
+
+let lastVal = 0; // bcz: WHY do I have to do this?? the pointerdown event locks in the value of 'valLoc' when it's created so need some other way to get the current value to that old handler...
+let lastEndVal = 0;
+
+export const Slider = (props: ISliderProps) => {
+ const [width, setWidth] = useState<number>(100);
+ const [valLoc, setNumberLoc] = useState<number>(props.number ?? props.min + (props.max - props.min) / 2);
+ const [endNumberLoc, setEndNumberLoc] = useState<number>(props.endNumber ?? props.min + (2 * (props.max - props.min)) / 3);
+ const [min, setMin] = useState<number>(props.min);
+ const [max, setMax] = useState<number>(props.max);
+ const {
+ formLabel,
+ formLabelPlacement,
+ multithumb,
+ autorange,
+ autorangeMinVal,
+ autorangeMinSize,
+ decimals,
+ step = 1,
+ number = valLoc,
+ endNumber = endNumberLoc,
+ minDiff = (max - min) / 20,
+ size = Size.SMALL,
+ height,
+ unit,
+ onPointerDown,
+ setNumber,
+ setEndNumber,
+ setFinalNumber,
+ setFinalEndNumber,
+ color = Colors.MEDIUM_BLUE,
+ fillWidth,
+ } = props;
+
+ const toDecimal = (num: number) => (decimals !== undefined ? Math.round(num * Math.pow(10, decimals)) / Math.pow(10, decimals) : num);
+
+ const getLeftPos = (locVal: number) => {
+ const dragger = getHeight(+(height || 0), size);
+ return ((locVal - min) / (max - min)) * (width - dragger);
+ };
+
+ const getValueLabel = (locVal: number): JSX.Element => {
+ return (
+ <div
+ className="rs-label-container"
+ style={{
+ left: `${getLeftPos(locVal)}px`,
+ background: color,
+ color: isDark(color) ? Colors.LIGHT_GRAY : Colors.DARK_GRAY,
+ fontSize: getFontSize(size),
+ height: getHeight(+(height || 0), size),
+ width: getHeight(+(height || 0), size),
+ top: 0,
+ }}>
+ <span className="rs-label">{toDecimal(locVal)}</span>
+ </div>
+ );
+ };
+ const checkAutorange = () => {
+ if (autorange) {
+ const minval = multithumb ? Math.min(lastVal, lastEndVal) : lastVal;
+ const maxval = multithumb ? Math.max(lastVal, lastEndVal) : lastVal;
+ const autosize = Math.max(autorangeMinSize ?? 0, autorange ?? maxval - minval) / 2;
+ if (Math.abs((minval - min) / (max - min)) < 0.15 || Math.abs((max - maxval) / (max - min)) < 0.15 || (multithumb && maxval - minval < (max - min) / 5 && autosize < max - min)) {
+ const newminval = autorangeMinVal !== undefined && minval - autosize < autorangeMinVal ? autorangeMinVal : minval - autosize;
+ setMin(newminval);
+ setMax(newminval !== minval ? Math.max(maxval + autosize, newminval + autosize) : maxval + autosize);
+ }
+ }
+ };
+
+ const valSlider = (which: string, val: number, onchange: (val: number) => void, setFinal: () => void) => {
+ const valPointerup = () => {
+ document.removeEventListener('pointerup', valPointerup, true);
+ setFinal();
+ checkAutorange();
+ };
+ return (
+ <div key={which} className={`range-slider ${size}`}>
+ {getValueLabel(val)}
+ <input
+ className={`rs-range ${size}`}
+ type="range"
+ color={color}
+ min={min}
+ max={max}
+ step={step}
+ value={val}
+ onPointerDown={() => document.addEventListener('pointerup', valPointerup, true)}
+ onChange={e => {
+ onchange(+e.target.value);
+ e.stopPropagation();
+ }}
+ />
+ </div>
+ );
+ };
+ const onchange = (val: number) => {
+ // eslint-disable-next-line no-param-reassign
+ if (autorangeMinVal && val < autorangeMinVal) val = autorangeMinVal;
+ setNumber?.((lastVal = Math.min(multithumb ? endNumber - (minDiff ?? 0) : Number.MAX_VALUE, val)));
+ setNumberLoc((lastVal = Math.min(multithumb ? endNumber - (minDiff ?? 0) : Number.MAX_VALUE, val)));
+ };
+ const onendchange = (val: number) => {
+ setEndNumber?.((lastEndVal = Math.max(number + (minDiff ?? 0), val)));
+ setEndNumberLoc((lastEndVal = Math.max(number + (minDiff ?? 0), val)));
+ };
+ const ValSlider: (JSX.Element | null)[] = [!multithumb ? null : valSlider('end', endNumberLoc, onendchange, () => setFinalEndNumber?.(lastEndVal)), valSlider('start', valLoc, onchange, () => setFinalNumber?.(lastVal))];
+
+ const slider = (
+ <div
+ className="slider-wrapper"
+ onPointerEnter={() => {
+ lastVal = valLoc;
+ lastEndVal = endNumberLoc;
+ }}
+ style={{
+ padding: `5px 0px ${getHeight(+(height || 0), size)}px 0px`,
+ width: fillWidth ? '100%' : 'fit-content',
+ }}>
+ <div
+ className="slider-container"
+ ref={r => {
+ r && new ResizeObserver(() => setWidth(+(r?.clientWidth ?? 100))).observe(r);
+ setWidth(+(r?.clientWidth ?? 100));
+ }}
+ style={{ height: getHeight(+(height || 0), size) }}
+ onPointerDown={onPointerDown}>
+ {ValSlider}
+ <div
+ className="selected-range"
+ style={{
+ height: getHeight(+(height || 0), size) / 10,
+ background: multithumb ? Colors.LIGHT_GRAY : color,
+ }}
+ />
+ <div
+ className="range"
+ style={{
+ height: getHeight(+(height || 0), size) / 10,
+ width: getLeftPos(endNumber) - getLeftPos(number),
+ left: getLeftPos(number) + getHeight(+(height || 0), size),
+ display: multithumb ? undefined : 'none',
+ background: color,
+ }}
+ />
+ <div className="box-minmax" style={{ fontSize: getFontSize(size), color }}>
+ <span>
+ {toDecimal(min)}
+ {unit}
+ </span>
+ <span>
+ {toDecimal(max)}
+ {unit}
+ </span>
+ </div>
+ </div>
+ </div>
+ );
+
+ return formLabel ? (
+ <div className={`form-wrapper ${formLabelPlacement}`}>
+ <div className="formLabel" style={{ fontSize: getFormLabelSize(size) }}>
+ {formLabel}
+ </div>
+ {slider}
+ </div>
+ ) : (
+ slider
+ );
+};