File size: 3,400 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
import React from 'react';
import * as Ariakit from '@ariakit/react';
import type { OptionWithIcon } from '~/common';
import { cn } from '~/utils';

type ComboboxProps = {
  label?: string;
  placeholder?: string;
  options: OptionWithIcon[] | string[];
  className?: string;
  labelClassName?: string;
  value: string;
  onChange: (value: string) => void;
  onBlur: () => void;
};

export const InputCombobox: React.FC<ComboboxProps> = ({
  label,
  labelClassName,
  placeholder = 'Select an option',
  options,
  className,
  value,
  onChange,
  onBlur,
}) => {
  const isOptionObject = (option: unknown): option is OptionWithIcon => {
    return option != null && typeof option === 'object' && 'value' in option;
  };

  const [isOpen, setIsOpen] = React.useState(false);
  const [inputValue, setInputValue] = React.useState(value);
  const [isKeyboardFocus, setIsKeyboardFocus] = React.useState(false);

  React.useEffect(() => {
    setInputValue(value);
  }, [value]);

  const handleChange = (newValue: string) => {
    setInputValue(newValue);
    onChange(newValue);
  };

  return (
    <Ariakit.ComboboxProvider value={inputValue} setValue={handleChange}>
      {label != null && (
        <Ariakit.ComboboxLabel
          className={cn('mb-2 block text-sm font-medium text-text-primary', labelClassName ?? '')}
        >
          {label}
        </Ariakit.ComboboxLabel>
      )}
      <div className={cn('relative', isKeyboardFocus ? 'rounded-md ring-2 ring-ring-primary' : '')}>
        <Ariakit.Combobox
          placeholder={placeholder}
          className={cn(
            'h-10 w-full rounded-md border border-border-light bg-surface-primary px-3 py-2 text-sm',
            'placeholder-text-secondary hover:bg-surface-hover',
            'focus:outline-none',
            className,
          )}
          onChange={(event) => handleChange(event.target.value)}
          onBlur={() => {
            setIsKeyboardFocus(false);
            onBlur();
          }}
          onFocusVisible={() => {
            setIsKeyboardFocus(true);
            setIsOpen(true);
          }}
          onMouseDown={() => {
            setIsKeyboardFocus(false);
          }}
        />
      </div>
      <Ariakit.ComboboxPopover
        gutter={4}
        sameWidth
        open={isOpen}
        onClose={() => setIsOpen(false)}
        className={cn(
          'z-50 max-h-60 w-full overflow-auto rounded-md bg-surface-primary p-1 shadow-lg',
          'animate-in fade-in-0 zoom-in-95',
        )}
      >
        {options.map((option: string | OptionWithIcon, index: number) => (
          <Ariakit.ComboboxItem
            key={index}
            className={cn(
              'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',
              'cursor-pointer hover:bg-surface-tertiary hover:text-text-primary',
              'data-[active-item]:bg-surface-tertiary data-[active-item]:text-text-primary',
            )}
            value={isOptionObject(option) ? `${option.value ?? ''}` : option}
          >
            {isOptionObject(option) && option.icon != null && (
              <span className="mr-2 flex-shrink-0">{option.icon}</span>
            )}
            {isOptionObject(option) ? option.label : option}
          </Ariakit.ComboboxItem>
        ))}
      </Ariakit.ComboboxPopover>
    </Ariakit.ComboboxProvider>
  );
};