forked from jordanlambrecht/tracker-tracker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathNumberInput.tsx
More file actions
119 lines (111 loc) · 3.24 KB
/
Copy pathNumberInput.tsx
File metadata and controls
119 lines (111 loc) · 3.24 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
// src/components/ui/NumberInput.tsx
//
// Functions: NumberInput
"use client"
import clsx from "clsx"
import { useId } from "react"
import { ChevronDownSmallIcon, ChevronUpSmallIcon } from "@/components/ui/Icons"
interface NumberInputProps {
value: number
onChange: (value: number) => void
min?: number
max?: number
step?: number
disabled?: boolean
label?: string
id?: string
className?: string
}
function NumberInput({
value,
onChange,
min = 1,
max = 99,
step = 1,
disabled = false,
label,
id,
className,
}: NumberInputProps) {
const generatedId = useId()
const inputId = id ?? generatedId
function clamp(v: number) {
return Math.min(max, Math.max(min, v))
}
function handleChange(raw: string) {
const parsed = parseInt(raw, 10)
if (!Number.isNaN(parsed)) {
onChange(clamp(parsed))
}
}
return (
<div className={clsx("inline-flex flex-col gap-1", className)}>
{label && (
<label
htmlFor={inputId}
className="text-xs font-sans font-medium text-secondary uppercase tracking-wider"
>
{label}
</label>
)}
<div className="inline-flex items-stretch nm-inset-sm overflow-hidden rounded-nm-sm">
<input
id={inputId}
type="text"
inputMode="numeric"
pattern="[0-9]*"
role="spinbutton"
value={value}
onChange={(e) => handleChange(e.target.value)}
disabled={disabled}
className={clsx(
"w-12 text-center font-mono text-xs text-primary",
"bg-control-bg py-2 px-1",
"border-0 focus:outline-none",
"disabled:opacity-40 disabled:cursor-not-allowed"
)}
aria-valuenow={value}
aria-valuemin={min}
aria-valuemax={max}
/>
<div className="flex flex-col border-l border-border">
<button
type="button"
tabIndex={-1}
disabled={disabled || value >= max}
onClick={() => onChange(clamp(value + step))}
className={clsx(
"flex items-center justify-center w-7 flex-1",
"bg-control-bg text-tertiary",
"hover:text-primary hover:bg-overlay",
"transition-colors duration-100 cursor-pointer",
"disabled:opacity-30 disabled:cursor-not-allowed",
"border-b border-border"
)}
aria-label="Increase"
>
<ChevronUpSmallIcon width="10" height="6" />
</button>
<button
type="button"
tabIndex={-1}
disabled={disabled || value <= min}
onClick={() => onChange(clamp(value - step))}
className={clsx(
"flex items-center justify-center w-7 flex-1",
"bg-control-bg text-tertiary",
"hover:text-primary hover:bg-overlay",
"transition-colors duration-100 cursor-pointer",
"disabled:opacity-30 disabled:cursor-not-allowed"
)}
aria-label="Decrease"
>
<ChevronDownSmallIcon width="10" height="6" />
</button>
</div>
</div>
</div>
)
}
export type { NumberInputProps }
export { NumberInput }