@kiwicom/orbit-components
Version:
Orbit-components is a React component library which provides developers with the easiest possible way of building Kiwi.com’s products.
332 lines • 12.4 kB
JavaScript
import * as React from "react";
import styled, { css, withTheme } from "styled-components";
import transition from "../utils/transition";
import Text from "../Text";
import Heading from "../Heading";
import Stack from "../Stack";
import Hide from "../Hide";
import Handle from "./components/Handle";
import Bar from "./components/Bar";
import KEY_CODE_MAP from "../common/keyMaps";
import DEFAULT_VALUES from "./consts";
import Histogram from "./components/Histogram";
import defaultTheme from "../defaultTheme";
import mq from "../utils/mediaQuery";
import { sortArray, findClosestKey, pauseEvent, stopPropagation, replaceValue, alignValue, injectCallbackAndSetState, moveValueByExtraStep, calculateValueFromPosition, isNotEqual } from "./utils";
const StyledSlider = styled.div.withConfig({
displayName: "Slider__StyledSlider",
componentId: "sc-a8zzil-0"
})(["position:relative;"]);
StyledSlider.defaultProps = {
theme: defaultTheme
};
const StyledSliderContent = styled.div.withConfig({
displayName: "Slider__StyledSliderContent",
componentId: "sc-a8zzil-1"
})(["", ""], ({
theme,
focused
}) => css(["display:block;width:100%;box-sizing:border-box;padding-bottom:", ";", ";"], theme.orbit.spaceXSmall, mq.tablet(css(["width:calc(100% + 48px);position:absolute;bottom:-16px;left:-24px;right:-24px;opacity:0;visibility:hidden;padding:12px 24px 48px 24px;border-radius:", ";transition:", ";background:transparent;", ";"], theme.orbit.borderRadiusNormal, transition(["all"], "fast", "ease-in-out"), focused && css(["visibility:visible;opacity:1;background:", ";box-shadow:", ";"], theme.orbit.paletteWhite, theme.orbit.boxShadowRaised)))));
StyledSliderContent.defaultProps = {
theme: defaultTheme
};
const StyledSliderInput = styled.div.withConfig({
displayName: "Slider__StyledSliderInput",
componentId: "sc-a8zzil-2"
})(["display:flex;align-items:center;width:100%;height:24px;"]);
const PureSlider = ({
defaultValue = DEFAULT_VALUES.VALUE,
maxValue = DEFAULT_VALUES.MAX,
minValue = DEFAULT_VALUES.MIN,
step = DEFAULT_VALUES.STEP,
theme,
onChange,
onChangeAfter,
onChangeBefore,
ariaValueText,
ariaLabel,
label,
histogramData,
histogramLoading,
histogramDescription,
histogramLoadingText,
valueDescription,
id,
dataTest
}) => {
const bar = React.useRef(null);
const [value, setValue] = React.useState(defaultValue);
const valueRef = React.useRef(value);
const defaultRef = React.useRef(defaultValue);
const handleIndex = React.useRef(null);
const [focused, setFocused] = React.useState(false);
const {
rtl
} = theme;
const updateValue = newValue => {
valueRef.current = newValue;
setValue(newValue);
};
React.useEffect(() => {
const newValue = Array.isArray(defaultValue) ? defaultValue.map(item => Number(item)) : Number(defaultValue);
if (isNotEqual(defaultValue, defaultRef.current)) {
defaultRef.current = newValue;
updateValue(newValue);
}
}, [defaultValue]);
const handleKeyDown = event => {
if (event.ctrlKey || event.shiftKey || event.altKey) return;
const eventCode = Number(event.code);
if (eventCode === KEY_CODE_MAP.ARROW_UP) {
pauseEvent(event);
if (onChange) {
injectCallbackAndSetState(updateValue, onChange, moveValueByExtraStep(value, maxValue, minValue, step, handleIndex.current, step));
}
}
if (eventCode === KEY_CODE_MAP.ARROW_DOWN) {
pauseEvent(event);
if (onChange) {
injectCallbackAndSetState(updateValue, onChange, moveValueByExtraStep(value, maxValue, minValue, step, handleIndex.current, -step));
}
}
if (eventCode === KEY_CODE_MAP.ARROW_RIGHT) {
const switchStep = rtl ? -step : step;
pauseEvent(event);
if (onChange) {
injectCallbackAndSetState(updateValue, onChange, moveValueByExtraStep(value, maxValue, minValue, step, handleIndex.current, switchStep));
}
}
if (eventCode === KEY_CODE_MAP.ARROW_LEFT) {
const switchStep = rtl ? step : -step;
pauseEvent(event);
if (onChange) {
injectCallbackAndSetState(updateValue, onChange, moveValueByExtraStep(value, maxValue, minValue, step, handleIndex.current, switchStep));
}
}
if (eventCode === KEY_CODE_MAP.HOME) {
pauseEvent(event);
if (onChange) {
injectCallbackAndSetState(updateValue, onChange, moveValueByExtraStep(value, maxValue, minValue, step, handleIndex.current, 0, minValue));
}
}
if (eventCode === KEY_CODE_MAP.END) {
pauseEvent(event);
if (onChange) {
injectCallbackAndSetState(updateValue, onChange, moveValueByExtraStep(value, maxValue, minValue, step, handleIndex.current, 0, maxValue));
}
}
};
const handleBlur = () => {
setFocused(false);
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("focusout", handleBlur);
if (onChangeAfter) {
injectCallbackAndSetState(updateValue, onChangeAfter, value);
}
};
const handleOnFocus = i => event => {
handleIndex.current = i;
setFocused(true);
pauseEvent(event);
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("focusout", handleBlur);
if (onChangeBefore) {
injectCallbackAndSetState(updateValue, onChangeBefore, value);
}
};
const handleMove = newValue => {
if (newValue != null) {
if (Array.isArray(value)) {
return replaceValue(valueRef.current, alignValue(maxValue, minValue, step, newValue), Number(handleIndex.current));
}
return alignValue(maxValue, minValue, step, newValue);
}
return null;
};
const handleBarMouseDown = event => {
handleIndex.current = null;
const newValue = calculateValueFromPosition({
histogramData,
histogramLoading,
maxValue,
minValue,
handleIndex: handleIndex.current,
bar,
rtl,
value,
pageX: event.pageX,
throughClick: true
});
if (newValue) {
if (Array.isArray(value)) {
const index = findClosestKey(newValue, value);
const replacedValue = replaceValue(value, alignValue(maxValue, minValue, step, newValue), index || 0);
if (onChangeBefore) injectCallbackAndSetState(updateValue, onChangeBefore, value);
if (onChange) injectCallbackAndSetState(updateValue, onChange, replacedValue);
if (onChangeAfter) injectCallbackAndSetState(updateValue, onChangeAfter, replacedValue);
} else {
const alignedValue = alignValue(maxValue, minValue, step, newValue);
if (onChangeBefore) injectCallbackAndSetState(updateValue, onChangeBefore, value);
if (onChange) injectCallbackAndSetState(updateValue, onChange, alignedValue);
if (onChangeAfter) injectCallbackAndSetState(updateValue, onChangeAfter, alignedValue);
}
}
};
const handleMouseMove = event => {
const newValue = calculateValueFromPosition({
histogramData,
histogramLoading,
maxValue,
minValue,
handleIndex: handleIndex.current,
bar,
rtl,
value,
pageX: event.pageX
});
pauseEvent(event);
injectCallbackAndSetState(updateValue, onChange, handleMove(newValue));
};
const handleMouseUp = () => {
setFocused(false);
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
if (onChangeAfter) {
injectCallbackAndSetState(updateValue, onChangeAfter, valueRef.current);
}
};
const handleMouseDown = i => event => {
// just allow left-click
if (event.button === 0 && event.buttons !== 2) {
setFocused(true);
handleIndex.current = i;
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
pauseEvent(event);
if (onChangeBefore) {
injectCallbackAndSetState(updateValue, onChangeBefore, value);
}
}
};
const handleOnTouchMove = event => {
if (event.touches.length > 1) return;
const newValue = calculateValueFromPosition({
histogramData,
histogramLoading,
maxValue,
minValue,
handleIndex: handleIndex.current,
bar,
rtl,
value,
pageX: event.touches[0]?.pageX || 0
});
pauseEvent(event);
injectCallbackAndSetState(updateValue, onChange, handleMove(newValue));
};
const handleTouchEnd = () => {
setFocused(false);
window.removeEventListener("touchmove", handleOnTouchMove);
window.removeEventListener("touchend", handleTouchEnd);
if (onChangeAfter) {
injectCallbackAndSetState(updateValue, onChangeAfter, valueRef.current);
}
};
const handleOnTouchStart = i => event => {
if (event.touches.length <= 1) {
setFocused(true);
handleIndex.current = i;
window.addEventListener("touchmove", handleOnTouchMove, {
passive: false
});
window.addEventListener("touchend", handleTouchEnd);
stopPropagation(event);
if (onChangeBefore) {
injectCallbackAndSetState(updateValue, onChangeBefore, value);
}
}
};
const renderHandle = i => {
const key = i && encodeURIComponent(i.toString());
const index = i || 0;
return /*#__PURE__*/React.createElement(Handle, {
tabIndex: "0",
onTop: handleIndex.current === i,
valueMax: maxValue,
valueMin: minValue,
onMouseDown: handleMouseDown(index),
onFocus: handleOnFocus(index),
onTouchStart: handleOnTouchStart(index),
value: valueRef.current,
ariaValueText: ariaValueText,
ariaLabel: ariaLabel,
hasHistogram: histogramLoading || !!histogramData,
index: index,
key: key,
dataTest: `SliderHandle-${index}`
});
};
const renderHandles = () => Array.isArray(value) ? value.map((_valueNow, index) => renderHandle(index)) : renderHandle();
const renderSliderTexts = React.useCallback(biggerSpace => {
if (!(label || valueDescription || histogramDescription)) return null;
return /*#__PURE__*/React.createElement(Stack, {
direction: "row",
spacing: "none",
spaceAfter: biggerSpace ? "medium" : "small"
}, (label || histogramDescription) && /*#__PURE__*/React.createElement(Stack, {
direction: "column",
spacing: "none",
basis: "60%",
grow: true
}, label && /*#__PURE__*/React.createElement(Heading, {
type: "title4"
}, label), valueDescription && /*#__PURE__*/React.createElement(Text, {
type: "secondary",
size: "small"
}, valueDescription)), histogramDescription && /*#__PURE__*/React.createElement(Stack, {
shrink: true,
justify: "end",
grow: false
}, /*#__PURE__*/React.createElement(Text, {
type: "primary",
size: "small"
}, histogramDescription)));
}, [histogramDescription, label, valueDescription]);
if (histogramData) {
const properHistogramLength = (maxValue - minValue + step) / step;
if (histogramData.length !== properHistogramLength) {
console.warn(`Warning: Length of histogramData array is ${histogramData.length}, but should be ${properHistogramLength}. This will cause broken visuals of the whole Histogram.`);
}
}
const sortedValue = sortArray(valueRef.current);
const hasHistogram = histogramLoading || !!histogramData;
return /*#__PURE__*/React.createElement(StyledSlider, {
"data-test": dataTest,
id: id
}, hasHistogram ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Hide, {
on: ["smallMobile", "mediumMobile", "largeMobile"],
block: true
}, renderSliderTexts(true)), /*#__PURE__*/React.createElement(StyledSliderContent, {
focused: focused
}, renderSliderTexts(false), /*#__PURE__*/React.createElement(Histogram, {
data: histogramData,
value: sortedValue,
min: minValue,
step: step,
loading: histogramLoading,
loadingText: histogramLoadingText
}))) : renderSliderTexts(true), /*#__PURE__*/React.createElement(StyledSliderInput, null, /*#__PURE__*/React.createElement(Bar, {
ref: bar,
onMouseDown: handleBarMouseDown,
value: sortedValue,
max: maxValue,
min: minValue,
hasHistogram: hasHistogram
}), renderHandles()));
};
PureSlider.defaultProps = {
theme: defaultTheme
};
const ThemedSlider = withTheme(PureSlider);
ThemedSlider.displayName = "Slider";
export default ThemedSlider;