Slider 컴포넌트 구현하기

김승규·2023년 6월 21일
0

디자인시스템

목록 보기
7/10
post-thumbnail

Slider 컴포넌트 구현 과정을 공유하고자 한다

결과물

배포된 환경에서 보기

Slider

Slider 컴포넌트 구현기

Slider 를 구현하면서 Slider 컴포넌트와 hover 시 툴팁 UI 와 나와야 하므로 SliderTooltip 컴포너트를 구현하였다. 한 번에 설명하는 것보다 분리시켜 설멍하는게 좋다고 생각하여 해당 컴포넌트와 스타일링을 같이 설명하고자 한다.

SliderTooltip 컴포넌트

// Slider.tsx

type SliderTooltipProps = {
  sliderValue: number;
  vertical: boolean;
};

function SliderTooltip({
  sliderValue, //
  vertical,
  children,
}: PropsWithChildren<SliderTooltipProps>) {
  return (
    <S.SliderTooltip
      data-slider-tooltip
      className={cns({ vertical })}
      style={
        {
          '--slider-value': `${sliderValue}%`,
        } as CSSProperties
      }
    >
      {children}
    </S.SliderTooltip>
  );
}
  • 슬라이더에서 사용하는 툴팁으로 슬라이더가 움직이면 해당 위치도 같이 이동해야하기 때문에 sliderValue(--slider-value) 를 전달하였다.
  • 기본적으로 가로 UI 인데 세로 기능도 제공하기 위해 vertical props 를 전달하여 판별했다.
export const SliderTooltip = styled.div`
  padding: 4px 6px;
  color: ${color.gray300};
  white-space: nowrap;
  background-color: ${color.gray800};
  border-radius: 0.25rem;
  outline: none;
  opacity: 0;
  font-size: 11px;
  line-height: 1.4;

  position: absolute;
  left: var(--slider-value);
  transform: translateX(0%) translateY(-175%);
  &.vertical {
    transform: translateY(50%) rotate(90deg);
  }
`;
  • Slider 툴팁 스타일을 지정하고 Slider 가 이동하면 툴팁도 같이 이동해야하기 때문에 position 을 absolute 로 지정하고 슬라이도가 이동하면 left: var(--slider-value); 을 통해 이동되게 하였다.
  • vertical 처리 부분을 고민하였는데 기존 UI 에서 rotate 로 회전시켜 처리했다.
    하지만 해당 방법은 좋은 방법 같지는 않다. 추후에 리팩토링할 예정이다.
    찾아보니 input 의 -webkit-appearance: slider-vertical 속성들을 통해 제어할 수 있다고 판단된다.

Slider 컴포넌트 구현

// Slider.tsx

import { CSSProperties, PropsWithChildren, useState } from 'react';

import * as S from './Slider.styles';
import { theme } from '@/styles/theme.ts';
import cns from 'classnames';

// ... SliderTooltip 구현 내용

export type SliderProps = {
  /** 슬라이더의 초기 값 0 ~ 100 */
  initialValue?: number;
  /** 슬라이더의 너비 */
  width?: string;
  /** vertical 일 때 slider 높이 */
  height?: string;
  /** 슬라이더 메인 컬러 */
  color?: string;
  /** 슬라이더의 라벨 아이템 0 ~ 100 */
  items?: number[];
  /** */
  vertical?: boolean;
};

export function Slider({
  initialValue, //
  width = '200px',
  height = '200px',
  color = theme.color.primary,
  items = [],
  vertical = false,
}: SliderProps) {
  const [sliderValue, setSliderValue] = useState(initialValue || 0);

  return (
    <S.SliderWrapper
      className={cns({ vertical })}
      style={
        {
          '--slider-width': width,
          '--slider-height': height,
        } as CSSProperties
      }
    >
      <S.Slider
        type="range" //
        value={sliderValue}
        color={color}
        onChange={(event) => setSliderValue(Number(event.target.value))}
        style={
          {
            '--slider-value': `${sliderValue}%`,
            '--slider-color': color,
          } as CSSProperties
        }
      />
      <SliderTooltip sliderValue={sliderValue} vertical={vertical}>
        {sliderValue}
      </SliderTooltip>

      {Array.isArray(items) && (
        <S.Labels>
          {items?.map((item) => (
            <S.Label
              value={item} //
              color={color}
              onClick={() => setSliderValue(Number(item))}
            >
              {item}%
            </S.Label>
          ))}
        </S.Labels>
      )}
    </S.SliderWrapper>
  );
}
  • SliderWrapper 는 Tooltip 을 같이 제공하기 위해 실제 Slider 의 Wrapper 요소로 구현하였다.
    • wrapper 이기 때문에 width, height 밑 vertical 여부를 제어하였다.
  • Slider 는 input 의 range 타입을 이용하여 Slider 를 구현하였다.
    onChage 를 이용해 slider 값이 변경되는 것을 처리했다.
  • SliderTooltip 은 slider 를 hover 하거나 이동할 때 현재 위치의 값을 툴팁형태로 보여주기 위한 컴포넌트이다
  • Labels 컴포넌트는 Slider 의 위치기반 값을 보여주기 위한 라벨 요소이다.
// Slider.styles.tsx
export const SliderWrapper = styled.div`
  position: relative;
  z-index: 0;
  user-select: none;
  width: var(--slider-width);
  height: var(--slider-height);

  &.vertical {
    transform: rotate(-90deg);
    width: var(--slider-height);
  }
`;

export const Slider = styled.input`
  width: 100%;
  -webkit-appearance: none;
  position: relative;
  background-color: rgba(0, 0, 0, 0.1);
  cursor: pointer;

  &[type='range'] {
    -webkit-appearance: none;
  }
  &::-webkit-slider-runnable-track {
    height: 5px;
    background: linear-gradient(
      to right,
      var(--slider-color) 0%,
      var(--slider-color) var(--slider-value),
      rgba(0, 0, 0, 0.1) 0%
    );
  }
  &::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 1.5rem;
    height: 1.5rem;
    border: 3px solid white;
    border-radius: 50%;
    background-color: var(--slider-color);
    position: absolute;
    top: -8px;
    left: var(--slider-value);
    cursor: pointer;
  }

  &:hover ~ [data-slider-tooltip],
  &:active ~ [data-slider-tooltip] {
    opacity: 1;
  }
`;

export const Labels = styled.ul`
  margin: 0.5rem 0 0;
  padding: 0;
  width: 100%;
  display: flex;
  justify-content: space-between;
`;

export const Label = styled.button(({ value, color }) => ({
  outline: 'none',
  border: 'none',
  backgroundColor: 'rgba(0, 0, 0, 0.15)',
  color: 'rgba(0, 0, 0, 0.25)',
  fontWeight: 600,
  padding: '0.3rem 0.7rem',
  borderRadius: '10px',
  position: 'absolute',
  left: `calc(${value}% - 20px)`,
  '&:hover': {
    backgroundColor: `${color}`,
    color: `${theme.color.gray700}`,
  },
  transform: 'translateY(50%) rotate(90deg)',
}));

[SliderWrapper 스타일]

export const SliderWrapper = styled.div`
  position: relative;
  z-index: 0;
  user-select: none;
  width: var(--slider-width);
  height: var(--slider-height);

  &.vertical {
    transform: rotate(-90deg);
    width: var(--slider-height);
  }
`;
  • SliderWrapper 스타일은 기준이 되는 요소이기 때문에 relative 로 지정했고, horizontal, vertical 에 따라 widt, height 값이 달라져야하기에 정의하였다.
  • 또한, horizontal 을 기준으로 되어있는데 vertical 인 경우 rotate 로 구현했기에 의미적으로 height 가 맞지만, width 로 슬라이더 형태를 구현하도록 하였다.
    (해당 부분은 -webkit-appearance: slider-vertical 속성들로 구현하면 좀 더 잘 제어할 수 있을거 같다)

[Slider]

export const Slider = styled.input`
  width: 100%;
  -webkit-appearance: none;
  position: relative;
  background-color: rgba(0, 0, 0, 0.1);
  cursor: pointer;

  &[type='range'] {
    -webkit-appearance: none;
  }
  &::-webkit-slider-runnable-track {
    height: 5px;
    background: linear-gradient(
      to right,
      var(--slider-color) 0%,
      var(--slider-color) var(--slider-value),
      rgba(0, 0, 0, 0.1) 0%
    );
  }
  &::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 1.5rem;
    height: 1.5rem;
    border: 3px solid white;
    border-radius: 50%;
    background-color: var(--slider-color);
    position: absolute;
    top: -8px;
    left: var(--slider-value);
    cursor: pointer;
  }

  &:hover ~ [data-slider-tooltip],
  &:active ~ [data-slider-tooltip] {
    opacity: 1;
  }
`;
  • Slider 를 구현할 때 input 요소의 range 로 구현하는 것이 a11y 에 더 적합하다고 생각하여 input 을 이용하여 구현하였다.

  • input 의 range 의 기본 스타일이 있어 -webkit-appearance: none; 으로 스타일을 초기화했다.
    :-webkit-slider-runnable-track &::-webkit-slider-thumb 속성으로 ragne 스타일 요소를 제어할 수 있어 제어했다.

    • :-webkit-slider-runnable-track 은 slider-thumb이 움직이는 트랙인 요소를 스타일링한다.
    • linear-gradiant 로 직선의 그라데이션 설정했다.
      • to-right 로 오른쪽에서 시작하도록 설정
      • var(--slider-color) 0%, 는 color 색상으로 시작하도록 설정
      • var(--slider-color) var(--slider-value) 은 해당 색상이 --slider-value(ex 50%) 까지 해당 color 로 채우고 이후에는 검은색으로 사라진다.
      • rgba(0, 0, 0, 0.1) 0% 은 검은색으로 사라지는 정도를 표현하였다. (해당 값으로 완전 검은색이 아닌 슬라이더 기본 색상이 보이도록 투명하게 설정했다.
  • &::-webkit-slider-thumb 은 slider-thumb 의 선택기로 슬라이더의 손잡이?의 스타일을 정의할 때 사용할 수 있다.

  • 마지막으로 Slider hover, active 를 한 경우 slider-tooltip 이 보이도록 설정했다.

[SliderLabel]

{Array.isArray(items) && (
  <S.Labels>
    {items?.map((item) => (
      <S.Label
        value={item} //
        color={color}
        onClick={() => setSliderValue(Number(item))}
        >
        {item}%
      </S.Label>
    ))}
  </S.Labels>
)}
export const Labels = styled.ul`
  margin: 0.5rem 0 0;
  padding: 0;
  width: 100%;
  display: flex;
  justify-content: space-between;
`;

export const Label = styled.button(({ value, color }) => ({
  outline: 'none',
  border: 'none',
  backgroundColor: 'rgba(0, 0, 0, 0.15)',
  color: 'rgba(0, 0, 0, 0.25)',
  fontWeight: 600,
  padding: '0.3rem 0.7rem',
  borderRadius: '10px',
  position: 'absolute',
  left: `calc(${value}% - 20px)`,
  '&:hover': {
    backgroundColor: `${color}`,
    color: `${theme.color.gray700}`,
  },
  transform: 'translateY(50%) rotate(90deg)',
}));
  • 사용자에게 받은 라벨 items 를 받아 라벨 정보들을 그려주는 역할이다.
  • 해당 부분도 vertical 인 경우 세로 방향으로 그려져야하기 때문에 rotate(90deg) 로 회전 시켰다.

::-webkit-slider-runnable-track 표준이 아니고,현재는 chrome 스타일링만 적용했다. 개선할 점이 너무 많아 보인다!
지금은 바닐라로 구현했는데 괜찮은 라이브러리 도움을 받는 것도 좋은 것 같다. - @reach/slider

reference

0개의 댓글