@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.
563 lines (489 loc) • 18 kB
JavaScript
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
import * as React from "react";
import styled, { css, withTheme } from "styled-components";
import convertHexToRgba from "@kiwicom/orbit-design-tokens/lib/convertHexToRgba";
import { warning } from "@kiwicom/js";
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 boundingClientRect from "../utils/boundingClientRect";
const StyledSlider = styled.div.withConfig({
displayName: "Slider__StyledSlider",
componentId: "j475w2-0"
})(["position:relative;"]);
StyledSlider.defaultProps = {
theme: defaultTheme
};
const StyledSliderContent = styled.div.withConfig({
displayName: "Slider__StyledSliderContent",
componentId: "j475w2-1"
})(["display:block;width:100%;box-sizing:border-box;padding-bottom:", ";", ";"], ({
theme
}) => 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:all ", " ease-in-out;background:transparent;", ";"], ({
theme
}) => theme.orbit.borderRadiusNormal, ({
theme
}) => theme.orbit.durationFast, ({
focused,
theme
}) => focused && css(["visibility:visible;opacity:1;background:", ";box-shadow:0 2px 4px 0 ", ",0 4px 12px 0 ", ";"], theme.orbit.paletteWhite, convertHexToRgba(theme.orbit.paletteInkLight, 24), convertHexToRgba(theme.orbit.paletteInkLight, 32)))));
StyledSliderContent.defaultProps = {
theme: defaultTheme
};
const StyledSliderInput = styled.div.withConfig({
displayName: "Slider__StyledSliderInput",
componentId: "j475w2-2"
})(["display:flex;align-items:center;width:100%;height:24px;"]);
export class PureSlider extends React.PureComponent {
constructor(...args) {
super(...args);
_defineProperty(this, "bar", React.createRef());
_defineProperty(this, "state", {
value: this.props.defaultValue || DEFAULT_VALUES.VALUE,
handleIndex: null,
focused: false
});
_defineProperty(this, "pauseEvent", event => {
if (typeof event.stopPropagation === "function") {
event.stopPropagation();
}
if (typeof event.preventDefault === "function" && (typeof event.cancelable !== "boolean" || event.cancelable)) {
event.preventDefault();
}
});
_defineProperty(this, "stopPropagation", event => {
if (typeof event.stopPropagation === "function") event.stopPropagation();
});
_defineProperty(this, "isNotEqual", (a, b) => {
if (Array.isArray(a) && Array.isArray(b)) {
return a.toString() !== b.toString();
}
return a !== b;
});
_defineProperty(this, "calculateValue", (ratio, addition, deduction) => {
const {
max = DEFAULT_VALUES.MAX,
min = DEFAULT_VALUES.MIN
} = this.props;
return Math.round((max - min + (addition ? 1 : 0)) * ratio + min - (deduction ? 1 : 0));
});
_defineProperty(this, "calculateValueFromPosition", (pageX, throughClick) => {
const barRect = boundingClientRect(this.bar);
if (barRect) {
const {
histogramData,
histogramLoading,
theme: {
rtl
}
} = this.props;
const {
handleIndex,
value
} = this.state;
const mousePosition = (rtl ? barRect.right : pageX) - (rtl ? pageX : barRect.left);
const positionRatio = mousePosition / barRect.width;
const hasHistogram = histogramLoading || !!histogramData; // when range slider
if (Array.isArray(value)) {
if (value[0] === value[value.length - 1]) {
if (this.calculateValue(positionRatio, true, true) >= value[value.length - 1]) {
return this.calculateValue(positionRatio, true, true);
}
return this.calculateValue(positionRatio, true);
}
if (this.isNotEqual(this.sortArray(value), value)) {
if (handleIndex === 0) {
return this.calculateValue(positionRatio, true, true);
}
return this.calculateValue(positionRatio, true);
}
const closestKey = this.findClosestKey(this.calculateValue(positionRatio), this.sortArray(value)); // when first handle of range slider or when clicked and it should move the first handle
if (handleIndex === 0 || throughClick && closestKey === 0) {
return this.calculateValue(positionRatio, true);
}
} // simple slider without histogram
if (handleIndex === null && !hasHistogram) {
return this.calculateValue(positionRatio);
}
return this.calculateValue(positionRatio, true, true);
}
return null;
});
_defineProperty(this, "sortArray", arr => {
if (Array.isArray(arr)) {
return arr.slice().sort((a, b) => a - b);
}
return arr;
});
_defineProperty(this, "findClosestKey", (goal, value) => {
return Array.isArray(value) ? value.reduce((acc, curr, index) => {
return Array.isArray(value) && Math.abs(curr - goal) < Math.abs(value[acc] - goal) ? index : acc;
}, 0) : null;
});
_defineProperty(this, "moveValueByStep", (step, forcedValue) => {
const {
value,
handleIndex
} = this.state;
if (Array.isArray(value)) {
return this.replaceValue(forcedValue || this.alignValue(value[Number(handleIndex)] + step), Number(handleIndex));
}
return forcedValue || this.alignValue(value + step);
});
_defineProperty(this, "handleKeyDown", event => {
if (event.ctrlKey || event.shiftKey || event.altKey) return;
const {
step = DEFAULT_VALUES.STEP,
min = DEFAULT_VALUES.MIN,
max = DEFAULT_VALUES.MAX,
theme: {
rtl
}
} = this.props;
if (event.keyCode === KEY_CODE_MAP.ARROW_UP) {
this.pauseEvent(event);
this.injectCallbackAndSetState(this.props.onChange, this.moveValueByStep(step));
}
if (event.keyCode === KEY_CODE_MAP.ARROW_DOWN) {
this.pauseEvent(event);
this.injectCallbackAndSetState(this.props.onChange, this.moveValueByStep(-step));
}
if (event.keyCode === KEY_CODE_MAP.ARROW_RIGHT) {
const switchStep = rtl ? -step : step;
this.pauseEvent(event);
this.injectCallbackAndSetState(this.props.onChange, this.moveValueByStep(switchStep));
}
if (event.keyCode === KEY_CODE_MAP.ARROW_LEFT) {
const switchStep = rtl ? step : -step;
this.pauseEvent(event);
this.injectCallbackAndSetState(this.props.onChange, this.moveValueByStep(switchStep));
}
if (event.keyCode === KEY_CODE_MAP.HOME) {
this.pauseEvent(event);
this.injectCallbackAndSetState(this.props.onChange, this.moveValueByStep(0, min));
}
if (event.keyCode === KEY_CODE_MAP.END) {
this.pauseEvent(event);
this.injectCallbackAndSetState(this.props.onChange, this.moveValueByStep(0, max));
}
});
_defineProperty(this, "handleBlur", () => {
this.setState({
focused: false
});
const {
value
} = this.state;
window.removeEventListener("keydown", this.handleKeyDown);
window.removeEventListener("focusout", this.handleBlur);
this.injectCallbackAndSetState(this.props.onChangeAfter, value, true);
});
_defineProperty(this, "handleOnFocus", i => event => {
if (typeof i === "number") this.setState({
handleIndex: i
});
const {
value
} = this.state;
this.setState({
focused: true
});
this.pauseEvent(event);
window.addEventListener("keydown", this.handleKeyDown);
window.addEventListener("focusout", this.handleBlur);
this.injectCallbackAndSetState(this.props.onChangeBefore, value, true);
});
_defineProperty(this, "handleMove", newValue => {
const {
value,
handleIndex
} = this.state;
if (newValue != null) {
if (Array.isArray(value)) {
return this.replaceValue(this.alignValue(newValue), Number(handleIndex));
}
return this.alignValue(newValue);
}
return null;
});
_defineProperty(this, "handleBarMouseDown", event => {
const {
value
} = this.state;
this.setState({
handleIndex: null
});
const newValue = this.calculateValueFromPosition(event.pageX, true);
if (newValue) {
if (Array.isArray(value)) {
const index = this.findClosestKey(newValue, value);
this.injectCallbackAndSetState(this.props.onChange, this.replaceValue(this.alignValue(newValue), index));
} else {
this.injectCallbackAndSetState(this.props.onChange, this.alignValue(newValue));
}
}
});
_defineProperty(this, "injectCallbackAndSetState", (callback, newValue, forced = false) => {
const {
value
} = this.state;
if (newValue != null) {
if (this.isNotEqual(newValue, value) || forced) {
this.setState({
value: newValue
});
if (callback) {
callback(this.sortArray(newValue));
}
}
}
});
_defineProperty(this, "handleMouseMove", event => {
const newValue = this.calculateValueFromPosition(event.pageX);
this.pauseEvent(event);
this.injectCallbackAndSetState(this.props.onChange, this.handleMove(newValue));
});
_defineProperty(this, "handleMouseUp", () => {
const {
value
} = this.state;
this.setState({
focused: false
});
window.removeEventListener("mousemove", this.handleMouseMove);
window.removeEventListener("mouseup", this.handleMouseUp);
this.injectCallbackAndSetState(this.props.onChangeAfter, value, true);
});
_defineProperty(this, "handleMouseDown", i => event => {
// just allow left-click
if (event.button === 0 && event.buttons !== 2) {
const {
value
} = this.state;
this.setState({
focused: true
});
if (typeof i === "number") this.setState({
handleIndex: i
});
window.addEventListener("mousemove", this.handleMouseMove);
window.addEventListener("mouseup", this.handleMouseUp);
this.pauseEvent(event);
this.injectCallbackAndSetState(this.props.onChangeBefore, value, true);
}
});
_defineProperty(this, "handleOnTouchMove", event => {
if (event.touches.length > 1) return;
const newValue = this.calculateValueFromPosition(event.touches[0].pageX);
this.pauseEvent(event);
this.injectCallbackAndSetState(this.props.onChange, this.handleMove(newValue));
});
_defineProperty(this, "handleTouchEnd", () => {
const {
value
} = this.state;
this.setState({
focused: false
});
window.removeEventListener("touchmove", this.handleOnTouchMove);
window.removeEventListener("touchend", this.handleTouchEnd);
this.injectCallbackAndSetState(this.props.onChangeAfter, value, true);
});
_defineProperty(this, "handleOnTouchStart", i => event => {
if (event.touches.length <= 1) {
this.setState({
focused: true
});
const {
value
} = this.state;
if (typeof i === "number") this.setState({
handleIndex: i
});
window.addEventListener("touchmove", this.handleOnTouchMove, {
passive: false
});
window.addEventListener("touchend", this.handleTouchEnd);
this.stopPropagation(event);
this.injectCallbackAndSetState(this.props.onChangeBefore, value, true);
}
});
_defineProperty(this, "alignValueToStep", value => {
const {
step = DEFAULT_VALUES.STEP
} = this.props;
if (step === 1) return value;
const gap = value % step;
if (gap === 0) return value;
if (gap * 2 >= step) {
return value - gap + step;
}
return value - gap;
});
_defineProperty(this, "alignValueToMaxMin", value => {
const {
max = DEFAULT_VALUES.MAX,
min = DEFAULT_VALUES.MIN
} = this.props;
if (value > max) {
return max;
}
if (value < min) {
return min;
}
return value;
});
_defineProperty(this, "alignValue", value => this.alignValueToMaxMin(this.alignValueToStep(value)));
_defineProperty(this, "replaceValue", (newValue, index) => {
const {
value
} = this.state;
if (index == null || !Array.isArray(value)) return newValue;
return value.map((item, key) => key === index ? newValue : item);
});
_defineProperty(this, "renderHandle", (valueNow, i) => {
const {
min = DEFAULT_VALUES.MIN,
max = DEFAULT_VALUES.MAX,
histogramData,
histogramLoading,
ariaValueText,
ariaLabel
} = this.props;
const {
handleIndex,
value
} = this.state;
const key = i && encodeURIComponent(i.toString());
return React.createElement(Handle, {
tabIndex: "0",
onTop: handleIndex === i,
valueMax: max,
valueMin: min,
onMouseDown: this.handleMouseDown(i),
onFocus: this.handleOnFocus(i),
onTouchStart: this.handleOnTouchStart(i),
value: value,
ariaValueText: ariaValueText,
ariaLabel: ariaLabel,
hasHistogram: histogramLoading || !!histogramData,
index: i || 0,
key: key
});
});
_defineProperty(this, "renderHandles", () => {
const {
value
} = this.state;
return Array.isArray(value) ? value.map((valueNow, i) => this.renderHandle(valueNow, i)) : this.renderHandle(value);
});
_defineProperty(this, "renderSliderTexts", biggerSpace => {
const {
label,
valueDescription,
histogramDescription
} = this.props;
if (!(label || valueDescription || histogramDescription)) return null;
return React.createElement(Stack, {
direction: "row",
spacing: "none",
spaceAfter: biggerSpace ? "medium" : "small"
}, (label || histogramDescription) && React.createElement(Stack, {
direction: "column",
spacing: "none",
basis: "60%",
grow: true
}, label && React.createElement(Heading, {
type: "title4",
element: "div"
}, label), valueDescription && React.createElement(Text, {
type: "secondary",
size: "small"
}, valueDescription)), histogramDescription && React.createElement(Stack, {
shrink: true,
justify: "end",
grow: false
}, React.createElement(Text, {
type: "primary",
size: "small"
}, histogramDescription)));
});
_defineProperty(this, "renderHeading", hasHistogram => {
if (hasHistogram) {
return React.createElement(Hide, {
on: ["smallMobile", "mediumMobile", "largeMobile"],
block: true
}, this.renderSliderTexts(true));
}
return this.renderSliderTexts(true);
});
}
componentDidUpdate(prevProps) {
const {
defaultValue = DEFAULT_VALUES.VALUE
} = this.props;
const {
defaultValue: prevDefaultValue = DEFAULT_VALUES.VALUE
} = prevProps;
if (this.isNotEqual(prevDefaultValue, defaultValue)) {
const newValue = Array.isArray(defaultValue) ? defaultValue.map(item => Number(item)) : Number(defaultValue); // eslint-disable-next-line react/no-did-update-set-state
this.setState({
value: newValue
});
}
}
render() {
const {
min = DEFAULT_VALUES.MIN,
max = DEFAULT_VALUES.MAX,
histogramData,
histogramLoading = false,
histogramLoadingText,
dataTest,
step = DEFAULT_VALUES.STEP
} = this.props;
if (histogramData) {
const properHistogramLength = (max - min + step) / step;
warning(histogramData.length === properHistogramLength, `Warning: Length of histogramData array is ${histogramData.length}, but should be ${properHistogramLength}. This will cause broken visuals of the whole Histogram.`);
}
const {
value,
focused
} = this.state;
const sortedValue = this.sortArray(value);
const hasHistogram = histogramLoading || !!histogramData;
return React.createElement(StyledSlider, {
"data-test": dataTest
}, this.renderHeading(hasHistogram), hasHistogram && React.createElement(StyledSliderContent, {
focused: focused
}, this.renderSliderTexts(false), React.createElement(Histogram, {
data: histogramData,
value: sortedValue,
min: min,
step: step,
loading: histogramLoading,
loadingText: histogramLoadingText
})), React.createElement(StyledSliderInput, null, React.createElement(Bar, {
ref: this.bar,
onMouseDown: this.handleBarMouseDown,
value: sortedValue,
max: max,
min: min,
hasHistogram: hasHistogram
}), this.renderHandles()));
}
}
_defineProperty(PureSlider, "defaultProps", {
theme: defaultTheme
});
const ThemedSlider = withTheme(PureSlider);
ThemedSlider.displayName = "Slider";
export default ThemedSlider;