ハイパーマッスルエンジニア

Vim、ShellScriptについてよく書く

Reactでセグメントコントロール作る

にゅるっとした動きのものがなかったので作った。

Next.jsで動くのを目的に作ったので、styled-jsxを利用している。

SegmentControl.tsx

import css from 'styled-jsx/css'
import { useState, useEffect } from 'react'

const styles = css`
  .controls {
    display: inline-flex;
    justify-content: space-between;
    background: #e7e7e7;
    border-radius: 8px;
    overflow: hidden;
    position: relative;
    width: 100%;
  }
  .controls::before {
    content: '';
    background: #5465ff;
    border-radius: 8px;
    position: absolute;
    top: 0px;
    bottom: 0px;
    left: 0;
    transition: transform 0.3s ease, width 0.3s ease;
  }
  .segment {
    color: gray;
    position: relative;
    text-align: center;
  }
  .segment.active {
    color: #fff;
  }
  label {
    cursor: pointer;
    display: block;
    padding: 5px 0px;
    transition: color 0.5s ease;
  }
  input {
    opacity: 0;
    margin: 0;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    position: absolute;
    cursor: pointer;
    width: 100%;
    height: 100%;
  }
`

interface Props {
  segments: {
    label: string
    value: string
  }[]
  callback: (value: string, index: number) => void
  defaultIndex?: number
}

export const SegmentedControl: React.FC<Props> = ({
  segments,
  callback,
  defaultIndex = 0,
}) => {
  const [activeIndex, setActiveIndex] = useState(defaultIndex)
  const [segmentLeft, setSegmentLeft] = useState(0)
  const segmentWidth = `${100 / segments.length}%`

  useEffect(() => {
    const id = `${activeIndex}-${segments[activeIndex].value}`
    const segment = document.getElementById(id)
    const { width } = segment.getBoundingClientRect()
    setSegmentLeft(width * activeIndex)
  }, [activeIndex])

  const onChange = (value, index) => {
    setActiveIndex(index)
    callback(value, index)
  }

  return (
    <div className="controls">
      {segments.map((item, i) => {
        return (
          <div
            id={`${i}-${item.value}`}
            key={item.value}
            className={`segment ${i === activeIndex && 'active'}`}
          >
            <input
              type="radio"
              value={item.value}
              onChange={() => onChange(item.value, i)}
              checked={i === activeIndex}
            />
            <label htmlFor={item.label}>{item.label}</label>
          </div>
        )
      })}
      <style jsx>{styles}</style>
      <style jsx>{`
        .segment {
          min-width: ${segmentWidth};
        }
        .controls::before {
          width: ${segmentWidth};
          transform: translateX(${segmentLeft}px);
        }
      `}</style>
    </div>
  )
}

呼び出す側

import { SegmentedControl } from './SegmentControl'

const IndexPage = () => {
  const onSegmentChange = (value, index) => {
    console.log(value, index)
  }
  return (
    <div style={{ width: '20%', padding: '20px' }}>
      <SegmentedControl
        callback={onSegmentChange}
        defaultIndex={0}
        segments={[
          {
            label: 'First',
            value: 'first',
          },
          {
            label: 'Second',
            value: 'second',
          },
        ]}
      />
    </div>
  )
}
export default IndexPage

めちゃくちゃ参考にさせていただいたサイト

https://letsbuildui.dev/articles/building-a-segmented-control-component