{"version":3,"file":"SegmentedControl.cjs","sources":["../../../src/components/SegmentedControl/SegmentedControl.tsx"],"sourcesContent":["'use client'\n\nimport {\n  type ComponentProps,\n  type FC,\n  type MouseEvent,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { Button } from '../Button'\n\nexport type Option = {\n  /** 選択時に返される値 */\n  value: string\n  /** ボタンに表示する内容 */\n  content: ReactNode\n  /** ボタンの `aria-label` */\n  ariaLabel?: string\n  /** ボタンを disabled にするかどうか */\n  disabled?: boolean\n}\n\ntype AbstractProps = {\n  /** 選択肢の配列 */\n  options: Option[]\n  /** 選択中の値 */\n  value?: string | null\n  /** 選択肢を押下したときに発火するコールバック関数 */\n  onClickOption?: (value: string) => void\n  /** 各ボタンの大きさ */\n  size?: 'M' | 'S'\n}\ntype Props = AbstractProps & Omit<ComponentProps<'div'>, keyof AbstractProps>\n\nconst classNameGenerator = tv({\n  slots: {\n    container: 'smarthr-ui-SegmentedControl shr-inline-flex',\n    buttonGroup: '-shr-space-x-px',\n    button: [\n      'smarthr-ui-SegmentedControl-button',\n      'shr-m-0 shr-rounded-none',\n      'focus-visible:shr-focus-indicator',\n      'first:shr-rounded-bl-m first:shr-rounded-tl-m',\n      'last:shr-rounded-br-m last:shr-rounded-tr-m',\n    ],\n  },\n  variants: {\n    size: {\n      M: {\n        button: '[&:has(>_span_>_.smarthr-ui-Icon:only-child)]:shr-p-0.75',\n      },\n      S: {\n        button: 'shr-p-0.5',\n      },\n    },\n  },\n})\n\nexport const SegmentedControl: FC<Props> = ({\n  options,\n  value,\n  onClickOption,\n  size = 'M',\n  className,\n  ...rest\n}) => {\n  const [isFocused, setIsFocused] = useState(false)\n  const containerRef = useRef<HTMLDivElement>(null)\n  const classNames = useMemo(() => {\n    const { container, buttonGroup, button } = classNameGenerator()\n\n    return {\n      container: container({ className }),\n      buttonGroup: buttonGroup(),\n      button: button({ size }),\n    }\n  }, [className, size])\n\n  const onDelegateFocus = useCallback(() => setIsFocused(true), [])\n  const onDelegateBlur = useCallback(() => setIsFocused(false), [])\n\n  useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (!isFocused || !containerRef.current || !document.activeElement) {\n        return\n      }\n\n      const radios = Array.from(\n        containerRef.current.querySelectorAll('[role=\"radio\"]:not(:disabled)'),\n      )\n\n      if (radios.length < 2) {\n        return\n      }\n\n      const focusedIndex = radios.indexOf(document.activeElement)\n\n      if (focusedIndex === -1) {\n        return\n      }\n\n      switch (e.key) {\n        case 'Down':\n        case 'ArrowDown':\n        case 'Right':\n        case 'ArrowRight': {\n          const nextIndex = focusedIndex + 1\n          const nextRadio = radios[nextIndex % radios.length]\n\n          if (nextRadio instanceof HTMLButtonElement) {\n            nextRadio.focus()\n          }\n\n          break\n        }\n        case 'Up':\n        case 'ArrowUp':\n        case 'Left':\n        case 'ArrowLeft': {\n          const nextIndex = focusedIndex - 1\n          const nextRadio = radios[(nextIndex + radios.length) % radios.length]\n\n          if (nextRadio instanceof HTMLButtonElement) {\n            nextRadio.focus()\n          }\n\n          break\n        }\n      }\n    }\n\n    document.addEventListener('keydown', handleKeyDown)\n\n    return () => {\n      document.removeEventListener('keydown', handleKeyDown)\n    }\n  }, [isFocused])\n\n  const excludesSelected = useMemo(\n    () => !value || options.every((option) => option.value !== value),\n    [value, options],\n  )\n\n  const actualOnClickOption = useMemo(\n    () =>\n      onClickOption\n        ? (e: MouseEvent<HTMLButtonElement>) => onClickOption(e.currentTarget.value)\n        : undefined,\n    [onClickOption],\n  )\n\n  return (\n    <div\n      {...rest}\n      className={classNames.container}\n      onFocus={onDelegateFocus}\n      onBlur={onDelegateBlur}\n      ref={containerRef}\n      role=\"toolbar\"\n    >\n      <div role=\"radiogroup\" className={classNames.buttonGroup}>\n        {options.map((option, index) => (\n          <SegmentedControlButton\n            key={option.value}\n            option={option}\n            index={index}\n            onClick={actualOnClickOption}\n            size={size}\n            value={value}\n            isFocused={isFocused}\n            excludesSelected={excludesSelected}\n            className={classNames.button}\n          />\n        ))}\n      </div>\n    </div>\n  )\n}\n\nconst SegmentedControlButton: FC<\n  Pick<Props, 'size' | 'value'> & {\n    onClick: undefined | ((e: MouseEvent<HTMLButtonElement>) => void)\n    option: Props['options'][number]\n    index: number\n    isFocused: boolean\n    excludesSelected: boolean\n    className: string\n  }\n> = ({ onClick, size, value, option, index, isFocused, excludesSelected, className }) => {\n  const attrs = useMemo(() => {\n    const checked = value === option.value\n\n    return {\n      checked,\n      ariaChecked: checked && !!value,\n      variant: checked ? 'primary' : 'secondary',\n    } as const\n  }, [value, option.value])\n  const tabIndex = useMemo(() => {\n    if (isFocused) {\n      return -1\n    }\n\n    if (excludesSelected) {\n      return index === 0 ? 0 : -1\n    }\n\n    return attrs.checked ? 0 : -1\n  }, [excludesSelected, isFocused, attrs.checked, index])\n\n  return (\n    // eslint-disable-next-line smarthr/best-practice-for-interactive-element\n    <Button\n      value={option.value}\n      disabled={option.disabled}\n      tabIndex={tabIndex}\n      role=\"radio\"\n      aria-label={option.ariaLabel}\n      aria-checked={attrs.ariaChecked}\n      onClick={onClick}\n      variant={attrs.variant}\n      size={size}\n      className={className}\n    >\n      {option.content}\n    </Button>\n  )\n}\n"],"names":[],"mappings":";;;;;;;;;;AAwCA;AACE;AACE;AACA;AACA;;;;;;AAMC;AACF;AACD;AACE;AACE;AACE;AACD;AACD;AACE;AACD;AACF;AACF;AACF;;;AAWC;AACA;;;AAII;;AAEA;;AAEJ;AAEA;AACA;;AAGE;AACE;;;AAIA;AAIA;;;;AAMA;;;AAIA;AACE;AACA;AACA;;AAEE;;AAGA;;;;;AAMF;AACA;AACA;;AAEE;AACA;AAEA;;;;;;AAON;AAEA;AAEA;AACE;AACF;AACF;AAEA;AAKA;AAGM;AACA;;AA8BR;AAEA;AAUE;AACE;;;AAIE;;;;AAIJ;;;;;AAMI;;AAGF;AACF;;;;AAmBF;;"}