File size: 3,637 Bytes
f0743f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import React, { useState, useRef, useLayoutEffect, useCallback, memo } from 'react';
import { useLocalize } from '~/hooks';

interface Option {
  value: string;
  label: string;
  icon?: React.ReactNode;
}

interface RadioProps {
  options: Option[];
  value?: string;
  onChange?: (value: string) => void;
  disabled?: boolean;
  className?: string;
  fullWidth?: boolean;
}

const Radio = memo(function Radio({
  options,
  value,
  onChange,
  disabled = false,
  className = '',
  fullWidth = false,
}: RadioProps) {
  const localize = useLocalize();
  const [currentValue, setCurrentValue] = useState<string>(value ?? '');
  const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
  const [backgroundStyle, setBackgroundStyle] = useState<React.CSSProperties>({});

  const handleChange = (newValue: string) => {
    setCurrentValue(newValue);
    onChange?.(newValue);
  };

  const updateBackgroundStyle = useCallback(() => {
    const selectedIndex = options.findIndex((opt) => opt.value === currentValue);
    if (selectedIndex >= 0 && buttonRefs.current[selectedIndex]) {
      const selectedButton = buttonRefs.current[selectedIndex];
      const container = selectedButton?.parentElement;
      if (selectedButton && container) {
        const containerRect = container.getBoundingClientRect();
        const buttonRect = selectedButton.getBoundingClientRect();
        const offsetLeft = buttonRect.left - containerRect.left - 4;
        setBackgroundStyle({
          width: `${buttonRect.width}px`,
          transform: `translateX(${offsetLeft}px)`,
        });
      }
    }
  }, [currentValue, options]);

  useLayoutEffect(() => {
    updateBackgroundStyle();
  }, [updateBackgroundStyle]);

  useLayoutEffect(() => {
    if (value !== undefined) {
      setCurrentValue(value);
    }
  }, [value]);

  if (options.length === 0) {
    return (
      <div
        className="relative inline-flex items-center rounded-lg bg-muted p-1 opacity-50"
        role="radiogroup"
      >
        <span className="px-4 py-2 text-xs text-muted-foreground">
          {localize('com_ui_no_options')}
        </span>
      </div>
    );
  }

  const selectedIndex = options.findIndex((opt) => opt.value === currentValue);

  return (
    <div
      className={`relative ${fullWidth ? 'flex' : 'inline-flex'} items-center rounded-lg bg-muted p-1 ${className}`}
      role="radiogroup"
    >
      {selectedIndex >= 0 && (
        <div
          className="pointer-events-none absolute inset-y-1 rounded-md border border-border/50 bg-background shadow-sm transition-all duration-300 ease-out"
          style={backgroundStyle}
        />
      )}
      {options.map((option, index) => (
        <button
          key={option.value}
          ref={(el) => {
            buttonRefs.current[index] = el;
          }}
          type="button"
          role="radio"
          aria-checked={currentValue === option.value}
          onClick={() => handleChange(option.value)}
          disabled={disabled}
          className={`relative z-10 flex h-[34px] items-center justify-center gap-2 rounded-md px-4 text-sm font-medium transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
            currentValue === option.value ? 'text-foreground' : 'text-foreground'
          } ${disabled ? 'cursor-not-allowed opacity-50' : ''} ${fullWidth ? 'flex-1' : ''}`}
        >
          {option.icon && <span className="flex-shrink-0">{option.icon}</span>}
          <span className="whitespace-nowrap">{option.label}</span>
        </button>
      ))}
    </div>
  );
});

export default Radio;