@equinor/eds-core-react
Version:
The React implementation of the Equinor Design System
357 lines (353 loc) • 14.1 kB
JavaScript
import { forwardRef, useState, useEffect, useRef, useCallback } from 'react';
import styled, { css } from 'styled-components';
import { slider } from './Slider.tokens.js';
import { MinMax } from './MinMax.js';
import { Output } from './Output.js';
import { SliderInput } from './SliderInput.js';
import { bordersTemplate, useId } from '@equinor/eds-utils';
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
const {
entities: {
track,
handle,
dot,
output
}
} = slider;
const fakeTrackBg = css(["background-image:url(\"data:image/svg+xml,<svg xmlns='http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'><rect x='0' y='11' fill='", "' width='100%' height='4' rx='2' /></svg>\");background-size:cover;background-repeat:no-repeat;"], track.background);
const fakeTrackBgHover = css({
backgroundImage: `url("data:image/svg+xml,<svg xmlns='http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'><rect x='0' y='11' fill='${track.states.hover.background}' width='100%' height='4' rx='2' /></svg>")`
});
const trackFill = css(["grid-column:1 / span 2;grid-row:2;height:", ";margin-bottom:", ";align-self:end;content:'';"], track.height, track.spacings.bottom);
const wrapperGrid = css(["display:grid;grid-template-rows:max-content 24px;grid-template-columns:1fr 1fr;width:100%;position:relative;"]);
const RangeWrapper = styled.div.attrs(({
$min,
$max,
$valA,
$valB,
$disabled,
$hideActiveTrack,
$labelAlwaysOn,
style
}) => ({
'data-disabled': $disabled ? true : null,
style: {
'--a': $valA,
'--b': $valB,
'--min': $min,
'--max': $max,
'--showTooltip': $labelAlwaysOn ? 1 : 0,
'--background': $disabled ? track.entities.indicator.states.disabled.background : $hideActiveTrack ? 'transparent' : track.entities.indicator.background,
'--background-hover': $hideActiveTrack ? 'transparent' : track.entities.indicator.states.hover.background,
...style
}
})).withConfig({
displayName: "Slider__RangeWrapper",
componentId: "sc-n1grrg-0"
})(["--dif:calc(var(--max) - var(--min));--realWidth:calc(100% - 12px);", " ", " &::before,&::after{", ";background:var(--background);}&::before{margin-left:calc( calc(", " / 2) + (var(--a) - var(--min)) / var(--dif) * var(--realWidth) );width:calc((var(--b) - var(--a)) / var(--dif) * var(--realWidth));}&::after{margin-left:calc( calc(", " / 2) + (var(--b) - var(--min)) / var(--dif) * var(--realWidth) );width:calc((var(--a) - var(--b)) / var(--dif) * var(--realWidth));}&:has(:focus-visible),&:hover{& > output{--showTooltip:1;--tooltip-background:", ";}}@media (hover:hover) and (pointer:fine){&:hover:not([data-disabled]){", " &::before,&::after{background:var(--background-hover);}}}", ";", ";"], wrapperGrid, fakeTrackBg, trackFill, handle.width, handle.width, output.states.hover.background, fakeTrackBgHover, ({
$touchNavigation
}) => $touchNavigation && css(["& > input[type='range']{pointer-events:none;}& > input[type='range']::-webkit-slider-thumb{pointer-events:auto;}& > input[type='range']::-moz-range-thumb{pointer-events:auto;}"]), ({
$labelBelow
}) => $labelBelow && css(["& > output{top:calc(100% + 1px);bottom:unset;}"]));
const Wrapper = styled.div.attrs(({
$min,
$max,
value,
$disabled,
$hideActiveTrack,
$labelAlwaysOn,
style
}) => ({
'data-disabled': $disabled ? true : null,
style: {
'--min': $min,
'--max': $max,
'--value': value,
'--showTooltip': $labelAlwaysOn ? 1 : 0,
'--background': $disabled ? track.entities.indicator.states.disabled.background : $hideActiveTrack ? 'transparent' : track.entities.indicator.background,
'--background-hover': $hideActiveTrack ? 'transparent' : track.entities.indicator.states.hover.background,
...style
}
})).withConfig({
displayName: "Slider__Wrapper",
componentId: "sc-n1grrg-1"
})(["--dif:calc(var(--max) - var(--min));--realWidth:calc(100% - 12px);", " ", " &::after{", " background:var(--background)}&::after{margin-right:calc( (var(--max) - var(--value)) / var(--dif) * var(--realWidth) );margin-left:3px;}&:has(:focus-visible),&:hover{& > output{--showTooltip:1;--tooltip-background:", ";}}@media (hover:hover) and (pointer:fine){&:hover:not([data-disabled]){", " &::after{background:var(--background-hover);}}", ";}"], wrapperGrid, fakeTrackBg, trackFill, output.states.hover.background, fakeTrackBgHover, ({
$labelBelow
}) => $labelBelow && css(["& > output{top:calc(100% + 1px);bottom:unset;}"]));
const WrapperGroupLabel = styled.div.withConfig({
displayName: "Slider__WrapperGroupLabel",
componentId: "sc-n1grrg-2"
})(["grid-row:1;grid-column:1 / 3;"]);
const WrapperGroupLabelDots = styled(WrapperGroupLabel).withConfig({
displayName: "Slider__WrapperGroupLabelDots",
componentId: "sc-n1grrg-3"
})(["&:before,&:after{content:' ';display:block;position:absolute;width:", ";height:", ";background:", ";", ";bottom:", ";left:0;}&:after{right:0;left:auto;}"], dot.width, dot.height, slider.background, bordersTemplate(dot.border), dot.spacings.bottom);
const SrOnlyLabel = styled.label.withConfig({
displayName: "Slider__SrOnlyLabel",
componentId: "sc-n1grrg-4"
})(["position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0;"]);
const Slider = /*#__PURE__*/forwardRef(function Slider({
min = 0,
max = 100,
value = [40, 60],
outputFunction,
onChange,
onChangeCommitted,
minMaxDots = true,
minMaxValues = true,
labelAlwaysOn,
labelBelow,
step = 1,
disabled,
hideActiveTrack,
ariaLabelledby,
'aria-labelledby': ariaLabelledbyNative,
...rest
}, ref) {
const isNumber = !Array.isArray(value);
const isRangeSlider = !isNumber && value.length === 2;
const parsedValue = isNumber ? [value] : value;
const [initalValue, setInitalValue] = useState(parsedValue);
const [sliderValue, setSliderValue] = useState(parsedValue);
const [mousePressed, setMousePressed] = useState(false);
const [touchNavigation, setTouchNavigation] = useState(false);
useEffect(() => {
if (isRangeSlider) {
if (value[0] !== initalValue[0] || value[1] !== initalValue[1]) {
setInitalValue(value);
setSliderValue(value);
}
} else {
const numberValue = Number(value);
if (numberValue !== initalValue[0]) {
setInitalValue([numberValue]);
setSliderValue([numberValue]);
}
}
}, [value, initalValue, isRangeSlider]);
const minRange = useRef(null);
const maxRange = useRef(null);
const onValueChange = (event, valueArrIdx) => {
const changedValue = parseFloat(event.target.value);
if (isRangeSlider) {
const newValue = sliderValue.slice();
newValue[valueArrIdx] = changedValue;
//Prevent min/max values from crossing eachother
if (valueArrIdx === 0 && newValue[0] >= newValue[1]) {
newValue[0] = newValue[1];
maxRange.current?.focus();
}
if (valueArrIdx === 1 && newValue[1] <= newValue[0]) {
newValue[1] = newValue[0];
minRange.current?.focus();
}
setSliderValue(newValue);
if (onChange) {
// Callback for provided onChange func
onChange(event, newValue);
}
return;
}
setSliderValue([changedValue]);
if (onChange) {
// Callback for provided onChange func
onChange(event, [changedValue]);
}
};
const handleKeyUp = event => {
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
handleCommitedValue(event);
}
};
const handleCommitedValue = event => {
if (onChangeCommitted) {
onChangeCommitted(event, sliderValue);
}
};
const getFormattedText = text => {
return outputFunction ? outputFunction(text) : text;
};
const findClosestRange = event => {
const target = event.target;
if (target.type === 'output' || mousePressed) {
return;
}
let clientX;
if (event.type === 'touchstart') {
clientX = event.targetTouches[0].clientX;
setTouchNavigation(true);
} else if (event.type === 'mousemove') {
clientX = event.clientX;
setTouchNavigation(false);
}
const bounds = target.getBoundingClientRect();
const x = clientX - bounds.left;
const inputWidth = minRange.current.offsetWidth;
const minValue = parseFloat(minRange.current.value);
const maxValue = parseFloat(maxRange.current.value);
const diff = max - min;
const normX = x / inputWidth * diff + min;
const maxX = Math.abs(normX - maxValue);
const minX = Math.abs(normX - minValue);
if (minX > maxX) {
minRange.current.style.zIndex = '10';
maxRange.current.style.zIndex = '20';
} else {
minRange.current.style.zIndex = '20';
maxRange.current.style.zIndex = '10';
}
//special cases where both thumbs are all the way to the left or right
if (minValue === maxValue && minValue === min) {
minRange.current.style.zIndex = '10';
maxRange.current.style.zIndex = '20';
}
if (minValue === maxValue && maxValue === max) {
minRange.current.style.zIndex = '20';
maxRange.current.style.zIndex = '10';
}
};
const handleDragging = type => {
if (type === 'mousedown' || type === 'touchmove') {
setMousePressed(true);
} else {
setMousePressed(false);
}
};
let inputIdA = useId(null, 'inputA');
let inputIdB = useId(null, 'inputB');
let inputId = useId(null, 'thumb');
if (rest['id']) {
const overrideId = rest['id'];
inputIdA = `${overrideId}-thumb-a`;
inputIdB = `${overrideId}-thumb-b`;
inputId = `${overrideId}-thumb`;
}
const getAriaLabelledby = useCallback(() => {
if (ariaLabelledbyNative) return ariaLabelledbyNative;
if (ariaLabelledby) return ariaLabelledby;
return null;
}, [ariaLabelledbyNative, ariaLabelledby]);
return /*#__PURE__*/jsx(Fragment, {
children: isRangeSlider ? /*#__PURE__*/jsxs(RangeWrapper, {
...rest,
ref: ref,
role: "group",
"aria-labelledby": getAriaLabelledby(),
$valA: sliderValue[0],
$valB: sliderValue[1],
$max: max,
$min: min,
$disabled: disabled,
$hideActiveTrack: hideActiveTrack,
$labelAlwaysOn: labelAlwaysOn || touchNavigation,
$labelBelow: labelBelow,
$touchNavigation: touchNavigation,
onMouseMove: findClosestRange,
onTouchStartCapture: findClosestRange,
onTouchEnd: e => handleDragging(e.type),
onTouchMove: e => handleDragging(e.type),
onMouseDown: e => handleDragging(e.type),
onMouseUp: e => handleDragging(e.type),
children: [minMaxDots && /*#__PURE__*/jsx(WrapperGroupLabelDots, {}), /*#__PURE__*/jsx(SrOnlyLabel, {
htmlFor: inputIdA,
children: "Value A"
}), /*#__PURE__*/jsx(SliderInput, {
type: "range",
ref: minRange,
value: sliderValue[0],
max: max,
min: min,
"aria-valuemax": max,
"aria-valuemin": min,
"aria-valuenow": sliderValue[0],
"aria-valuetext": getFormattedText(sliderValue[0]).toString(),
id: inputIdA,
step: step,
onChange: event => {
onValueChange(event, 0);
},
onMouseUp: event => handleCommitedValue(event),
onTouchEnd: event => handleCommitedValue(event),
onKeyUp: event => handleKeyUp(event),
disabled: disabled
}), /*#__PURE__*/jsx(Output, {
htmlFor: inputIdA,
value: sliderValue[0],
children: getFormattedText(sliderValue[0])
}), minMaxValues && /*#__PURE__*/jsx(MinMax, {
children: getFormattedText(min)
}), /*#__PURE__*/jsx(SrOnlyLabel, {
htmlFor: inputIdB,
children: "Value B"
}), /*#__PURE__*/jsx(SliderInput, {
type: "range",
value: sliderValue[1],
min: min,
max: max,
"aria-valuemax": max,
"aria-valuemin": min,
"aria-valuenow": sliderValue[1],
"aria-valuetext": getFormattedText(sliderValue[1]).toString(),
id: inputIdB,
step: step,
ref: maxRange,
onChange: event => {
onValueChange(event, 1);
},
onMouseUp: event => handleCommitedValue(event),
onTouchEnd: event => handleCommitedValue(event),
onKeyUp: event => handleKeyUp(event),
disabled: disabled
}), /*#__PURE__*/jsx(Output, {
htmlFor: inputIdB,
value: sliderValue[1],
children: getFormattedText(sliderValue[1])
}), minMaxValues && /*#__PURE__*/jsx(MinMax, {
children: getFormattedText(max)
})]
}) : /*#__PURE__*/jsxs(Wrapper, {
...rest,
ref: ref,
$max: max,
$min: min,
value: sliderValue[0],
$disabled: disabled,
$hideActiveTrack: hideActiveTrack,
$labelAlwaysOn: labelAlwaysOn || touchNavigation,
$labelBelow: labelBelow,
onTouchStartCapture: () => setTouchNavigation(true),
onMouseDownCapture: () => setTouchNavigation(false),
children: [/*#__PURE__*/jsx(SliderInput, {
type: "range",
value: sliderValue[0],
min: min,
max: max,
"aria-valuemax": max,
"aria-valuemin": min,
"aria-valuenow": sliderValue[0],
"aria-valuetext": getFormattedText(sliderValue[0]).toString(),
step: step,
id: inputId,
onChange: event => {
onValueChange(event);
},
disabled: disabled,
"aria-labelledby": getAriaLabelledby(),
onMouseUp: event => handleCommitedValue(event),
onKeyUp: event => handleKeyUp(event),
onTouchEnd: event => handleCommitedValue(event)
}), /*#__PURE__*/jsx(Output, {
htmlFor: inputId,
value: sliderValue[0],
children: getFormattedText(sliderValue[0])
}), minMaxDots && /*#__PURE__*/jsx(WrapperGroupLabelDots, {}), minMaxValues && /*#__PURE__*/jsxs(Fragment, {
children: [/*#__PURE__*/jsx(MinMax, {
children: getFormattedText(min)
}), /*#__PURE__*/jsx(MinMax, {
children: getFormattedText(max)
})]
})]
})
});
});
export { Slider };