forked from jordanlambrecht/tracker-tracker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSelect.tsx
More file actions
123 lines (111 loc) · 3.32 KB
/
Copy pathSelect.tsx
File metadata and controls
123 lines (111 loc) · 3.32 KB
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
115
116
117
118
119
120
121
122
123
// src/components/ui/Select.tsx
"use client"
import clsx from "clsx"
import { useId, useRef, useState } from "react"
import { useClickOutside } from "@/hooks/useClickOutside"
type SelectSize = "sm" | "md"
interface SelectOption<T extends string> {
value: T
label: string
disabled?: boolean
}
interface SelectProps<T extends string> {
value: T
options: SelectOption<T>[]
onChange: (value: T) => void
ariaLabel: string
label?: string
size?: SelectSize
className?: string
}
const sizeStyles: Record<SelectSize, { trigger: string; option: string; radius: string }> = {
sm: {
trigger: "text-xs px-3 py-2 nm-inset-sm rounded-nm-sm",
option: "px-3 py-2 text-xs",
radius: "var(--radius-sm)",
},
md: {
trigger: "text-sm px-4 py-3 nm-inset rounded-nm-md",
option: "px-4 py-3 text-sm",
radius: "var(--radius-md)",
},
}
function Select<T extends string>({
value,
options,
onChange,
ariaLabel,
label,
size = "sm",
className,
}: SelectProps<T>) {
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
const generatedId = useId()
const s = sizeStyles[size]
const selectedLabel = options.find((o) => o.value === value)?.label ?? value
useClickOutside(ref, () => setOpen(false), open)
return (
<div ref={ref} className={clsx("relative flex-1 min-w-0", className)}>
{label && (
<label
htmlFor={generatedId}
className="text-xs font-sans font-medium text-secondary uppercase tracking-wider block mb-1"
>
{label}
</label>
)}
<button
type="button"
id={generatedId}
onClick={() => setOpen((o) => !o)}
className={clsx(
"w-full bg-control-bg text-primary font-mono flex items-center justify-between gap-1 cursor-pointer border-0",
s.trigger
)}
aria-label={ariaLabel}
aria-expanded={open}
>
<span className="truncate">{selectedLabel}</span>
<span className="text-tertiary text-3xs shrink-0" aria-hidden="true">
▾
</span>
</button>
{open && (
<div
className="absolute top-full left-0 right-0 mt-1 z-40 bg-elevated nm-raised-sm py-1 overflow-hidden rounded-nm-md"
role="listbox"
aria-label={ariaLabel}
>
{options.map((option) => (
<button
key={option.value}
type="button"
role="option"
aria-selected={option.value === value}
aria-disabled={option.disabled || undefined}
disabled={option.disabled}
onClick={() => {
onChange(option.value)
setOpen(false)
}}
className={clsx(
"w-full text-left font-mono transition-colors duration-100",
s.option,
option.disabled
? "text-muted cursor-not-allowed opacity-50"
: option.value === value
? "text-accent bg-accent-dim cursor-pointer"
: "text-secondary hover:text-primary hover:bg-overlay cursor-pointer"
)}
>
{option.label}
</button>
))}
</div>
)}
</div>
)
}
export type { SelectOption, SelectProps, SelectSize }
export { Select }