All files / src/components/forms ColorSlider.tsx

56.25% Statements 18/32
0% Branches 0/4
57.14% Functions 4/7
54.83% Lines 17/31

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 1193x 3x                 3x         3x                                           3x                 150x     3x           150x   150x   150x                     150x               150x                             150x     2875x 2875x 2875x                                     3x  
import React, { useCallback, useMemo, useRef } from "react";
import styled from "styled-components";
 
type ColorSliderProps = {
  value: number;
  steps: number;
  colorAtValue: (value: number) => string;
  onChange: (value: number) => void;
};
 
const Wrapper = styled.div`
  display: flex;
  position: relative;
`;
 
const Color = styled.div`
  flex-grow: 1;
  max-width: 32px;
  height: 28px;
  border: 1px solid transparent;
  box-sizing: border-box;
 
  &:first-of-type {
    border-top-left-radius: 4px;
    border-bottom-left-radius: 4px;
    min-width: 5px;
    flex-shrink: 0;
  }
 
  &:nth-last-of-type(2) {
    border-top-right-radius: 4px;
    border-bottom-right-radius: 4px;
    min-width: 5px;
    flex-shrink: 0;
  }
`;
 
const Handle = styled.div`
  position: absolute;
  width: 10px;
  margin-left: -5px;
  margin-top: -3px;
  height: 100%;
  border-radius: 4px;
  padding: 2px;
  box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.3);
  border: 1px solid ${(props) => props.theme.colors.input.background};
`;
 
const ColorSlider = ({
  value,
  onChange,
  steps,
  colorAtValue,
}: ColorSliderProps) => {
  const boundingRect = useRef<DOMRect | undefined>(undefined);
 
  const stepValues = useMemo(() => Array.from(Array(steps).keys()), [steps]);
 
  const onMouseMove = useCallback(
    (e: MouseEvent) => {
      if (boundingRect.current) {
        const x = e.clientX - boundingRect.current.left;
        const value = x / boundingRect.current.width;
        onChange(value);
      }
    },
    [onChange],
  );
 
  const onMouseUp = useCallback(
    (_e: MouseEvent) => {
      window.removeEventListener("mousemove", onMouseMove);
      window.removeEventListener("mouseup", onMouseUp);
    },
    [onMouseMove],
  );
 
  const onMouseDown = useCallback(
    (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
      if (!(e.currentTarget instanceof HTMLElement)) {
        return;
      }
      boundingRect.current = e.currentTarget.getBoundingClientRect();
      const x = e.clientX - boundingRect.current.left;
      const value = x / boundingRect.current.width;
      onChange(value);
      window.addEventListener("mousemove", onMouseMove);
      window.addEventListener("mouseup", onMouseUp);
    },
    [onChange, onMouseMove, onMouseUp],
  );
 
  return (
    <Wrapper onMouseDown={onMouseDown}>
      {stepValues.map((stepIndex) => {
        const normalisedValue = stepIndex / (stepValues.length - 1);
        const color = colorAtValue(normalisedValue);
        return (
          <Color
            key={stepIndex}
            style={{
              backgroundColor: color,
            }}
          />
        );
      })}
      <Handle
        style={{
          left: `${value * 100}%`,
          backgroundColor: colorAtValue(value),
        }}
      />
    </Wrapper>
  );
};
 
export default ColorSlider;