UNPKG

@equinor/eds-core-react

Version:

The React implementation of the Equinor Design System

357 lines (353 loc) 14.1 kB
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 };