@discourser/design-system 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -4
- package/dist/styles.css +5126 -0
- package/guidelines/Guidelines.md +67 -123
- package/guidelines/components/accordion.md +93 -0
- package/guidelines/components/avatar.md +70 -0
- package/guidelines/components/badge.md +61 -0
- package/guidelines/components/button.md +75 -40
- package/guidelines/components/card.md +84 -25
- package/guidelines/components/checkbox.md +88 -0
- package/guidelines/components/dialog.md +619 -31
- package/guidelines/components/drawer.md +655 -0
- package/guidelines/components/heading.md +71 -0
- package/guidelines/components/icon-button.md +92 -37
- package/guidelines/components/input-addon.md +685 -0
- package/guidelines/components/input-group.md +830 -0
- package/guidelines/components/input.md +92 -37
- package/guidelines/components/popover.md +71 -0
- package/guidelines/components/progress.md +63 -0
- package/guidelines/components/radio-group.md +95 -0
- package/guidelines/components/select.md +507 -0
- package/guidelines/components/skeleton.md +76 -0
- package/guidelines/components/slider.md +911 -0
- package/guidelines/components/spinner.md +783 -0
- package/guidelines/components/switch.md +105 -38
- package/guidelines/components/tabs.md +654 -0
- package/guidelines/components/textarea.md +70 -0
- package/guidelines/components/toast.md +77 -0
- package/guidelines/components/tooltip.md +80 -0
- package/guidelines/design-tokens/colors.md +309 -72
- package/guidelines/design-tokens/elevation.md +615 -45
- package/guidelines/design-tokens/spacing.md +654 -74
- package/guidelines/design-tokens/typography.md +432 -50
- package/guidelines/overview-components.md +9 -5
- package/guidelines/overview-imports.md +314 -0
- package/guidelines/overview-patterns.md +3852 -0
- package/package.json +4 -2
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
# InputAddon
|
|
2
|
+
|
|
3
|
+
**Purpose:** Decorative element for enhancing input fields with icons, text labels, or buttons following Material Design 3 patterns.
|
|
4
|
+
|
|
5
|
+
## When to Use This Component
|
|
6
|
+
|
|
7
|
+
Use InputAddon when you need to **add visual context or actions** to input fields (currency symbols, units, search icons, clear buttons).
|
|
8
|
+
|
|
9
|
+
**Decision Tree:**
|
|
10
|
+
|
|
11
|
+
| Scenario | Use This | Why |
|
|
12
|
+
| ------------------------------------------------------- | ---------------------- | ------------------------------------- |
|
|
13
|
+
| Add prefix/suffix text (currency, units, domains) | InputAddon ✅ | Visual context for input values |
|
|
14
|
+
| Add icons to inputs (search, user, email icons) | InputAddon ✅ | Visual affordance for input type |
|
|
15
|
+
| Add action buttons to inputs (clear, visibility toggle) | InputAddon with Button | Interactive enhancements |
|
|
16
|
+
| Standalone input without decoration | Input | No additional context needed |
|
|
17
|
+
| Input with floating label inside | Input | Built-in label feature |
|
|
18
|
+
| Complex input composition with multiple elements | InputGroup | Layout management for multiple addons |
|
|
19
|
+
|
|
20
|
+
**Component Comparison:**
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// ✅ Use InputAddon for currency prefix
|
|
24
|
+
<InputGroup.Root size="md">
|
|
25
|
+
<InputAddon variant="outline">$</InputAddon>
|
|
26
|
+
<Input placeholder="0.00" type="number" />
|
|
27
|
+
</InputGroup.Root>
|
|
28
|
+
|
|
29
|
+
// ✅ Use InputAddon for unit suffix
|
|
30
|
+
<InputGroup.Root size="md">
|
|
31
|
+
<Input placeholder="Enter weight" type="number" />
|
|
32
|
+
<InputAddon variant="outline">kg</InputAddon>
|
|
33
|
+
</InputGroup.Root>
|
|
34
|
+
|
|
35
|
+
// ✅ Use InputAddon for icons
|
|
36
|
+
<InputGroup.Root size="md">
|
|
37
|
+
<InputAddon variant="subtle">
|
|
38
|
+
<SearchIcon />
|
|
39
|
+
</InputAddon>
|
|
40
|
+
<Input placeholder="Search..." />
|
|
41
|
+
</InputGroup.Root>
|
|
42
|
+
|
|
43
|
+
// ❌ Don't use InputAddon alone - must be within InputGroup
|
|
44
|
+
<InputAddon>$</InputAddon> // Wrong - needs InputGroup wrapper
|
|
45
|
+
<Input placeholder="Price" />
|
|
46
|
+
|
|
47
|
+
// ❌ Don't use InputAddon for labels - use Input label prop
|
|
48
|
+
<InputGroup.Root>
|
|
49
|
+
<InputAddon>Email</InputAddon> // Wrong - this is a label
|
|
50
|
+
<Input />
|
|
51
|
+
</InputGroup.Root>
|
|
52
|
+
|
|
53
|
+
<Input label="Email" /> // Correct
|
|
54
|
+
|
|
55
|
+
// ❌ Don't use InputAddon for validation messages
|
|
56
|
+
<InputGroup.Root>
|
|
57
|
+
<Input />
|
|
58
|
+
<InputAddon variant="outline">Invalid email</InputAddon> // Wrong
|
|
59
|
+
</InputGroup.Root>
|
|
60
|
+
|
|
61
|
+
<Input errorText="Invalid email" /> // Correct
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Import
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { InputAddon, InputGroup, Input } from '@discourser/design-system';
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Component Structure
|
|
71
|
+
|
|
72
|
+
InputAddon must be used within InputGroup for proper positioning:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
<InputGroup.Root>
|
|
76
|
+
<InputAddon><!-- prefix content --></InputAddon>
|
|
77
|
+
<Input />
|
|
78
|
+
<InputAddon><!-- suffix content --></InputAddon>
|
|
79
|
+
</InputGroup.Root>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Variants
|
|
83
|
+
|
|
84
|
+
The InputAddon component supports 3 Material Design 3 variants:
|
|
85
|
+
|
|
86
|
+
| Variant | Visual Style | Usage | When to Use |
|
|
87
|
+
| --------- | ---------------------------------- | --------------- | -------------------------------- |
|
|
88
|
+
| `outline` | Border with transparent background | Standard addons | Default, matches outlined inputs |
|
|
89
|
+
| `surface` | Surface background with border | Elevated addons | Cards, elevated contexts |
|
|
90
|
+
| `subtle` | Subtle background, no border | Minimal addons | Search bars, minimal UI |
|
|
91
|
+
|
|
92
|
+
### Visual Characteristics
|
|
93
|
+
|
|
94
|
+
- **outline**: 1px border, transparent background, matches input border color
|
|
95
|
+
- **surface**: Surface background color with subtle border
|
|
96
|
+
- **subtle**: Light gray background, seamless integration with filled inputs
|
|
97
|
+
|
|
98
|
+
## Sizes
|
|
99
|
+
|
|
100
|
+
| Size | Height | Padding | Icon Size | Usage |
|
|
101
|
+
| ---- | ------ | ------- | --------- | ------------------------------- |
|
|
102
|
+
| `xs` | 32px | 8px | 16px | Extra compact forms, mobile |
|
|
103
|
+
| `sm` | 36px | 10px | 18px | Compact forms, dense layouts |
|
|
104
|
+
| `md` | 40px | 12px | 20px | Default, most use cases |
|
|
105
|
+
| `lg` | 44px | 14px | 20px | Touch targets, prominent inputs |
|
|
106
|
+
| `xl` | 48px | 16px | 22px | Large forms, hero sections |
|
|
107
|
+
|
|
108
|
+
**Recommendation:** Use `md` for most cases. Match the size with your Input component size.
|
|
109
|
+
|
|
110
|
+
## Props
|
|
111
|
+
|
|
112
|
+
| Prop | Type | Default | Description |
|
|
113
|
+
| ----------- | -------------------------------------- | ----------- | ----------------------------- |
|
|
114
|
+
| `variant` | `'outline' \| 'surface' \| 'subtle'` | `'outline'` | Visual style variant |
|
|
115
|
+
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Addon size (match with Input) |
|
|
116
|
+
| `children` | `ReactNode` | Required | Content (text, icon, button) |
|
|
117
|
+
| `className` | `string` | - | Additional CSS classes |
|
|
118
|
+
|
|
119
|
+
**Note:** InputAddon extends `HTMLAttributes<HTMLDivElement>`, so all standard HTML div attributes are supported.
|
|
120
|
+
|
|
121
|
+
## Examples
|
|
122
|
+
|
|
123
|
+
### Basic Text Addons
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { InputAddon, InputGroup, Input } from '@discourser/design-system';
|
|
127
|
+
|
|
128
|
+
// Currency prefix
|
|
129
|
+
<InputGroup.Root size="md">
|
|
130
|
+
<InputAddon variant="outline">$</InputAddon>
|
|
131
|
+
<Input placeholder="0.00" type="number" />
|
|
132
|
+
</InputGroup.Root>
|
|
133
|
+
|
|
134
|
+
// Domain suffix
|
|
135
|
+
<InputGroup.Root size="md">
|
|
136
|
+
<Input placeholder="username" />
|
|
137
|
+
<InputAddon variant="outline">@example.com</InputAddon>
|
|
138
|
+
</InputGroup.Root>
|
|
139
|
+
|
|
140
|
+
// Unit suffix
|
|
141
|
+
<InputGroup.Root size="md">
|
|
142
|
+
<Input placeholder="Enter distance" type="number" />
|
|
143
|
+
<InputAddon variant="outline">miles</InputAddon>
|
|
144
|
+
</InputGroup.Root>
|
|
145
|
+
|
|
146
|
+
// Both prefix and suffix
|
|
147
|
+
<InputGroup.Root size="md">
|
|
148
|
+
<InputAddon variant="outline">https://</InputAddon>
|
|
149
|
+
<Input placeholder="mywebsite" />
|
|
150
|
+
<InputAddon variant="outline">.com</InputAddon>
|
|
151
|
+
</InputGroup.Root>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Icon Addons
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { SearchIcon, UserIcon, LockIcon, CalendarIcon } from 'your-icon-library';
|
|
158
|
+
|
|
159
|
+
// Search icon prefix
|
|
160
|
+
<InputGroup.Root size="md">
|
|
161
|
+
<InputAddon variant="subtle">
|
|
162
|
+
<SearchIcon />
|
|
163
|
+
</InputAddon>
|
|
164
|
+
<Input placeholder="Search products..." />
|
|
165
|
+
</InputGroup.Root>
|
|
166
|
+
|
|
167
|
+
// User icon prefix
|
|
168
|
+
<InputGroup.Root size="md">
|
|
169
|
+
<InputAddon variant="outline">
|
|
170
|
+
<UserIcon />
|
|
171
|
+
</InputAddon>
|
|
172
|
+
<Input placeholder="Username" />
|
|
173
|
+
</InputGroup.Root>
|
|
174
|
+
|
|
175
|
+
// Lock icon for password
|
|
176
|
+
<InputGroup.Root size="md">
|
|
177
|
+
<InputAddon variant="outline">
|
|
178
|
+
<LockIcon />
|
|
179
|
+
</InputAddon>
|
|
180
|
+
<Input type="password" placeholder="Password" />
|
|
181
|
+
</InputGroup.Root>
|
|
182
|
+
|
|
183
|
+
// Calendar icon suffix
|
|
184
|
+
<InputGroup.Root size="md">
|
|
185
|
+
<Input type="date" />
|
|
186
|
+
<InputAddon variant="outline">
|
|
187
|
+
<CalendarIcon />
|
|
188
|
+
</InputAddon>
|
|
189
|
+
</InputGroup.Root>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Button Addons
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import { IconButton } from '@discourser/design-system';
|
|
196
|
+
import { XIcon, EyeIcon, EyeOffIcon } from 'your-icon-library';
|
|
197
|
+
|
|
198
|
+
// Clear button
|
|
199
|
+
const [value, setValue] = useState('');
|
|
200
|
+
|
|
201
|
+
<InputGroup.Root size="md">
|
|
202
|
+
<Input
|
|
203
|
+
placeholder="Search..."
|
|
204
|
+
value={value}
|
|
205
|
+
onChange={(e) => setValue(e.target.value)}
|
|
206
|
+
/>
|
|
207
|
+
{value && (
|
|
208
|
+
<InputAddon variant="subtle">
|
|
209
|
+
<IconButton
|
|
210
|
+
variant="ghost"
|
|
211
|
+
size="sm"
|
|
212
|
+
aria-label="Clear"
|
|
213
|
+
onClick={() => setValue('')}
|
|
214
|
+
>
|
|
215
|
+
<XIcon />
|
|
216
|
+
</IconButton>
|
|
217
|
+
</InputAddon>
|
|
218
|
+
)}
|
|
219
|
+
</InputGroup.Root>
|
|
220
|
+
|
|
221
|
+
// Password visibility toggle
|
|
222
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
223
|
+
|
|
224
|
+
<InputGroup.Root size="md">
|
|
225
|
+
<InputAddon variant="outline">
|
|
226
|
+
<LockIcon />
|
|
227
|
+
</InputAddon>
|
|
228
|
+
<Input
|
|
229
|
+
type={showPassword ? 'text' : 'password'}
|
|
230
|
+
placeholder="Password"
|
|
231
|
+
/>
|
|
232
|
+
<InputAddon variant="subtle">
|
|
233
|
+
<IconButton
|
|
234
|
+
variant="ghost"
|
|
235
|
+
size="sm"
|
|
236
|
+
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
|
237
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
238
|
+
>
|
|
239
|
+
{showPassword ? <EyeOffIcon /> : <EyeIcon />}
|
|
240
|
+
</IconButton>
|
|
241
|
+
</InputAddon>
|
|
242
|
+
</InputGroup.Root>
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Different Variants
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
// Outline variant (default)
|
|
249
|
+
<InputGroup.Root size="md">
|
|
250
|
+
<InputAddon variant="outline">$</InputAddon>
|
|
251
|
+
<Input placeholder="0.00" />
|
|
252
|
+
</InputGroup.Root>
|
|
253
|
+
|
|
254
|
+
// Surface variant
|
|
255
|
+
<InputGroup.Root size="md">
|
|
256
|
+
<InputAddon variant="surface">
|
|
257
|
+
<SearchIcon />
|
|
258
|
+
</InputAddon>
|
|
259
|
+
<Input placeholder="Search..." />
|
|
260
|
+
</InputGroup.Root>
|
|
261
|
+
|
|
262
|
+
// Subtle variant (seamless)
|
|
263
|
+
<InputGroup.Root size="md">
|
|
264
|
+
<InputAddon variant="subtle">
|
|
265
|
+
<UserIcon />
|
|
266
|
+
</InputAddon>
|
|
267
|
+
<Input placeholder="Username" />
|
|
268
|
+
</InputGroup.Root>
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Different Sizes
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
// Extra small
|
|
275
|
+
<InputGroup.Root size="xs">
|
|
276
|
+
<InputAddon size="xs">$</InputAddon>
|
|
277
|
+
<Input size="xs" placeholder="0.00" />
|
|
278
|
+
</InputGroup.Root>
|
|
279
|
+
|
|
280
|
+
// Small
|
|
281
|
+
<InputGroup.Root size="sm">
|
|
282
|
+
<InputAddon size="sm">$</InputAddon>
|
|
283
|
+
<Input size="sm" placeholder="0.00" />
|
|
284
|
+
</InputGroup.Root>
|
|
285
|
+
|
|
286
|
+
// Medium (default)
|
|
287
|
+
<InputGroup.Root size="md">
|
|
288
|
+
<InputAddon size="md">$</InputAddon>
|
|
289
|
+
<Input size="md" placeholder="0.00" />
|
|
290
|
+
</InputGroup.Root>
|
|
291
|
+
|
|
292
|
+
// Large
|
|
293
|
+
<InputGroup.Root size="lg">
|
|
294
|
+
<InputAddon size="lg">$</InputAddon>
|
|
295
|
+
<Input size="lg" placeholder="0.00" />
|
|
296
|
+
</InputGroup.Root>
|
|
297
|
+
|
|
298
|
+
// Extra large
|
|
299
|
+
<InputGroup.Root size="xl">
|
|
300
|
+
<InputAddon size="xl">$</InputAddon>
|
|
301
|
+
<Input size="xl" placeholder="0.00" />
|
|
302
|
+
</InputGroup.Root>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Multiple Addons
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
// Multiple icons
|
|
309
|
+
<InputGroup.Root size="md">
|
|
310
|
+
<InputAddon variant="outline">
|
|
311
|
+
<UserIcon />
|
|
312
|
+
</InputAddon>
|
|
313
|
+
<Input placeholder="Search users..." />
|
|
314
|
+
<InputAddon variant="subtle">
|
|
315
|
+
<SearchIcon />
|
|
316
|
+
</InputAddon>
|
|
317
|
+
</InputGroup.Root>
|
|
318
|
+
|
|
319
|
+
// Mixed content types
|
|
320
|
+
<InputGroup.Root size="md">
|
|
321
|
+
<InputAddon variant="outline">From:</InputAddon>
|
|
322
|
+
<Input type="date" />
|
|
323
|
+
<InputAddon variant="outline">To:</InputAddon>
|
|
324
|
+
<Input type="date" />
|
|
325
|
+
</InputGroup.Root>
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### With Form Labels
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
// Proper form structure
|
|
332
|
+
<div className={css({ display: 'flex', flexDirection: 'column', gap: 'xs' })}>
|
|
333
|
+
<label htmlFor="price-input" className={css({ fontWeight: 'medium', textStyle: 'sm' })}>
|
|
334
|
+
Price
|
|
335
|
+
</label>
|
|
336
|
+
<InputGroup.Root size="md">
|
|
337
|
+
<InputAddon variant="outline">$</InputAddon>
|
|
338
|
+
<Input id="price-input" placeholder="0.00" type="number" />
|
|
339
|
+
<InputAddon variant="outline">USD</InputAddon>
|
|
340
|
+
</InputGroup.Root>
|
|
341
|
+
<span className={css({ color: 'fg.muted', textStyle: 'xs' })}>
|
|
342
|
+
Enter the product price in US dollars
|
|
343
|
+
</span>
|
|
344
|
+
</div>
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Loading State
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
import { Spinner } from '@discourser/design-system';
|
|
351
|
+
|
|
352
|
+
const [isSearching, setIsSearching] = useState(false);
|
|
353
|
+
|
|
354
|
+
<InputGroup.Root size="md">
|
|
355
|
+
<InputAddon variant="subtle">
|
|
356
|
+
{isSearching ? (
|
|
357
|
+
<Spinner size="sm" />
|
|
358
|
+
) : (
|
|
359
|
+
<SearchIcon />
|
|
360
|
+
)}
|
|
361
|
+
</InputAddon>
|
|
362
|
+
<Input placeholder="Search..." />
|
|
363
|
+
</InputGroup.Root>
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Common Patterns
|
|
367
|
+
|
|
368
|
+
### Currency Input
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
const [amount, setAmount] = useState('');
|
|
372
|
+
|
|
373
|
+
<div className={css({ display: 'flex', flexDirection: 'column', gap: 'xs' })}>
|
|
374
|
+
<label htmlFor="amount" className={css({ fontWeight: 'medium' })}>
|
|
375
|
+
Amount
|
|
376
|
+
</label>
|
|
377
|
+
<InputGroup.Root size="md">
|
|
378
|
+
<InputAddon variant="outline">$</InputAddon>
|
|
379
|
+
<Input
|
|
380
|
+
id="amount"
|
|
381
|
+
type="number"
|
|
382
|
+
placeholder="0.00"
|
|
383
|
+
value={amount}
|
|
384
|
+
onChange={(e) => setAmount(e.target.value)}
|
|
385
|
+
min="0"
|
|
386
|
+
step="0.01"
|
|
387
|
+
/>
|
|
388
|
+
<InputAddon variant="outline">USD</InputAddon>
|
|
389
|
+
</InputGroup.Root>
|
|
390
|
+
</div>
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Search Bar
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
397
|
+
|
|
398
|
+
<InputGroup.Root size="md">
|
|
399
|
+
<InputAddon variant="subtle">
|
|
400
|
+
<SearchIcon />
|
|
401
|
+
</InputAddon>
|
|
402
|
+
<Input
|
|
403
|
+
placeholder="Search products, categories, brands..."
|
|
404
|
+
value={searchQuery}
|
|
405
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
406
|
+
/>
|
|
407
|
+
{searchQuery && (
|
|
408
|
+
<InputAddon variant="subtle">
|
|
409
|
+
<IconButton
|
|
410
|
+
variant="ghost"
|
|
411
|
+
size="sm"
|
|
412
|
+
aria-label="Clear search"
|
|
413
|
+
onClick={() => setSearchQuery('')}
|
|
414
|
+
>
|
|
415
|
+
<XIcon />
|
|
416
|
+
</IconButton>
|
|
417
|
+
</InputAddon>
|
|
418
|
+
)}
|
|
419
|
+
</InputGroup.Root>
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Website URL Input
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
const [url, setUrl] = useState('');
|
|
426
|
+
|
|
427
|
+
<InputGroup.Root size="md">
|
|
428
|
+
<InputAddon variant="outline">https://</InputAddon>
|
|
429
|
+
<Input
|
|
430
|
+
placeholder="example.com"
|
|
431
|
+
value={url}
|
|
432
|
+
onChange={(e) => setUrl(e.target.value)}
|
|
433
|
+
/>
|
|
434
|
+
</InputGroup.Root>
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Email Input with Domain
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
const [username, setUsername] = useState('');
|
|
441
|
+
|
|
442
|
+
<InputGroup.Root size="md">
|
|
443
|
+
<InputAddon variant="outline">
|
|
444
|
+
<MailIcon />
|
|
445
|
+
</InputAddon>
|
|
446
|
+
<Input
|
|
447
|
+
placeholder="username"
|
|
448
|
+
value={username}
|
|
449
|
+
onChange={(e) => setUsername(e.target.value)}
|
|
450
|
+
/>
|
|
451
|
+
<InputAddon variant="outline">@company.com</InputAddon>
|
|
452
|
+
</InputGroup.Root>
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Phone Number Input
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
const [phone, setPhone] = useState('');
|
|
459
|
+
|
|
460
|
+
<InputGroup.Root size="md">
|
|
461
|
+
<InputAddon variant="outline">
|
|
462
|
+
<PhoneIcon />
|
|
463
|
+
</InputAddon>
|
|
464
|
+
<InputAddon variant="outline">+1</InputAddon>
|
|
465
|
+
<Input
|
|
466
|
+
placeholder="(555) 000-0000"
|
|
467
|
+
type="tel"
|
|
468
|
+
value={phone}
|
|
469
|
+
onChange={(e) => setPhone(e.target.value)}
|
|
470
|
+
/>
|
|
471
|
+
</InputGroup.Root>
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
## DO NOT
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
// ❌ Don't use InputAddon without InputGroup
|
|
478
|
+
<InputAddon>$</InputAddon>
|
|
479
|
+
<Input placeholder="Price" /> // Wrong - InputAddon must be inside InputGroup
|
|
480
|
+
|
|
481
|
+
// ✅ Wrap both in InputGroup
|
|
482
|
+
<InputGroup.Root>
|
|
483
|
+
<InputAddon>$</InputAddon>
|
|
484
|
+
<Input placeholder="Price" />
|
|
485
|
+
</InputGroup.Root>
|
|
486
|
+
|
|
487
|
+
// ❌ Don't mismatch sizes
|
|
488
|
+
<InputGroup.Root size="md">
|
|
489
|
+
<InputAddon size="lg">$</InputAddon> // Wrong - size mismatch
|
|
490
|
+
<Input size="md" placeholder="0.00" />
|
|
491
|
+
</InputGroup.Root>
|
|
492
|
+
|
|
493
|
+
// ✅ Match sizes
|
|
494
|
+
<InputGroup.Root size="md">
|
|
495
|
+
<InputAddon size="md">$</InputAddon>
|
|
496
|
+
<Input size="md" placeholder="0.00" />
|
|
497
|
+
</InputGroup.Root>
|
|
498
|
+
|
|
499
|
+
// ❌ Don't use InputAddon for labels
|
|
500
|
+
<InputGroup.Root>
|
|
501
|
+
<InputAddon>Email Address</InputAddon> // Wrong - this is a label
|
|
502
|
+
<Input />
|
|
503
|
+
</InputGroup.Root>
|
|
504
|
+
|
|
505
|
+
// ✅ Use proper label element
|
|
506
|
+
<div>
|
|
507
|
+
<label>Email Address</label>
|
|
508
|
+
<InputGroup.Root>
|
|
509
|
+
<InputAddon><MailIcon /></InputAddon>
|
|
510
|
+
<Input />
|
|
511
|
+
</InputGroup.Root>
|
|
512
|
+
</div>
|
|
513
|
+
|
|
514
|
+
// ❌ Don't override colors with inline styles
|
|
515
|
+
<InputAddon style={{ backgroundColor: 'red' }}>$</InputAddon>
|
|
516
|
+
|
|
517
|
+
// ✅ Use variants
|
|
518
|
+
<InputAddon variant="surface">$</InputAddon>
|
|
519
|
+
|
|
520
|
+
// ❌ Don't put too much content in addon
|
|
521
|
+
<InputAddon variant="outline">
|
|
522
|
+
This is way too much text for an input addon
|
|
523
|
+
</InputAddon> // Wrong - keeps addons concise
|
|
524
|
+
|
|
525
|
+
// ✅ Keep content short
|
|
526
|
+
<InputAddon variant="outline">USD</InputAddon>
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
## Accessibility
|
|
530
|
+
|
|
531
|
+
The InputAddon component follows WCAG 2.1 Level AA standards:
|
|
532
|
+
|
|
533
|
+
- **Color Contrast**: All variants meet 4.5:1 contrast ratio for text
|
|
534
|
+
- **Icon Accessibility**: Icons are decorative when paired with input context
|
|
535
|
+
- **Interactive Elements**: Buttons within addons have proper labels
|
|
536
|
+
- **Visual Association**: Addons are visually grouped with inputs
|
|
537
|
+
- **Touch Targets**: Minimum 44x44px for interactive addons (size md or larger)
|
|
538
|
+
|
|
539
|
+
### Accessibility Best Practices
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
// ✅ Provide aria-label for icon-only buttons in addons
|
|
543
|
+
<InputGroup.Root>
|
|
544
|
+
<Input placeholder="Search..." />
|
|
545
|
+
<InputAddon variant="subtle">
|
|
546
|
+
<IconButton
|
|
547
|
+
variant="ghost"
|
|
548
|
+
size="sm"
|
|
549
|
+
aria-label="Clear search"
|
|
550
|
+
onClick={handleClear}
|
|
551
|
+
>
|
|
552
|
+
<XIcon />
|
|
553
|
+
</IconButton>
|
|
554
|
+
</InputAddon>
|
|
555
|
+
</InputGroup.Root>
|
|
556
|
+
|
|
557
|
+
// ✅ Use descriptive labels for inputs with addons
|
|
558
|
+
<div>
|
|
559
|
+
<label htmlFor="price">Price in USD</label>
|
|
560
|
+
<InputGroup.Root>
|
|
561
|
+
<InputAddon>$</InputAddon>
|
|
562
|
+
<Input id="price" type="number" />
|
|
563
|
+
</InputGroup.Root>
|
|
564
|
+
</div>
|
|
565
|
+
|
|
566
|
+
// ✅ Provide context for screen readers
|
|
567
|
+
<InputGroup.Root>
|
|
568
|
+
<InputAddon aria-label="Currency: US Dollar">$</InputAddon>
|
|
569
|
+
<Input
|
|
570
|
+
placeholder="0.00"
|
|
571
|
+
aria-label="Enter amount in US dollars"
|
|
572
|
+
/>
|
|
573
|
+
</InputGroup.Root>
|
|
574
|
+
|
|
575
|
+
// ✅ Ensure interactive addons are keyboard accessible
|
|
576
|
+
<InputGroup.Root>
|
|
577
|
+
<Input type="password" />
|
|
578
|
+
<InputAddon>
|
|
579
|
+
<IconButton
|
|
580
|
+
variant="ghost"
|
|
581
|
+
aria-label="Toggle password visibility"
|
|
582
|
+
onClick={toggleVisibility}
|
|
583
|
+
tabIndex={0} // Keyboard accessible
|
|
584
|
+
>
|
|
585
|
+
<EyeIcon />
|
|
586
|
+
</IconButton>
|
|
587
|
+
</InputAddon>
|
|
588
|
+
</InputGroup.Root>
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
## Variant Selection Guide
|
|
592
|
+
|
|
593
|
+
| Scenario | Recommended Variant | Reasoning |
|
|
594
|
+
| ----------------------- | ------------------- | ----------------------------- |
|
|
595
|
+
| Standard forms | `outline` | Matches outlined input style |
|
|
596
|
+
| Search bars | `subtle` | Minimal, seamless integration |
|
|
597
|
+
| Currency/unit labels | `outline` | Clear visual separation |
|
|
598
|
+
| Icon indicators | `subtle` | Less emphasis, decorative |
|
|
599
|
+
| Elevated surfaces/cards | `surface` | Matches surface context |
|
|
600
|
+
| Interactive buttons | `subtle` | Reduces visual weight |
|
|
601
|
+
|
|
602
|
+
## State Behaviors
|
|
603
|
+
|
|
604
|
+
| State | Visual Change | Behavior |
|
|
605
|
+
| ------------------------ | ----------------------- | ---------------------------- |
|
|
606
|
+
| **Default** | Matches variant styling | Static decoration |
|
|
607
|
+
| **With Input Focus** | No change | Addon remains static |
|
|
608
|
+
| **Interactive (Button)** | Hover/focus states | Button states apply |
|
|
609
|
+
| **Disabled Input** | Reduced opacity | Matches input disabled state |
|
|
610
|
+
|
|
611
|
+
## Responsive Considerations
|
|
612
|
+
|
|
613
|
+
```typescript
|
|
614
|
+
// Mobile-first: Use larger sizes for touch
|
|
615
|
+
<InputGroup.Root size={{ base: 'lg', md: 'md' }}>
|
|
616
|
+
<InputAddon size={{ base: 'lg', md: 'md' }}>$</InputAddon>
|
|
617
|
+
<Input size={{ base: 'lg', md: 'md' }} />
|
|
618
|
+
</InputGroup.Root>
|
|
619
|
+
|
|
620
|
+
// Compact on desktop, comfortable on mobile
|
|
621
|
+
<InputGroup.Root size={{ base: 'md', lg: 'sm' }}>
|
|
622
|
+
<InputAddon size={{ base: 'md', lg: 'sm' }}>
|
|
623
|
+
<SearchIcon />
|
|
624
|
+
</InputAddon>
|
|
625
|
+
<Input size={{ base: 'md', lg: 'sm' }} placeholder="Search..." />
|
|
626
|
+
</InputGroup.Root>
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
## Testing
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
import { render, screen } from '@testing-library/react';
|
|
633
|
+
import userEvent from '@testing-library/user-event';
|
|
634
|
+
|
|
635
|
+
test('addon displays text content', () => {
|
|
636
|
+
render(
|
|
637
|
+
<InputGroup.Root>
|
|
638
|
+
<InputAddon>$</InputAddon>
|
|
639
|
+
<Input placeholder="Amount" />
|
|
640
|
+
</InputGroup.Root>
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
expect(screen.getByText('$')).toBeInTheDocument();
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test('addon button triggers action', async () => {
|
|
647
|
+
const handleClear = vi.fn();
|
|
648
|
+
|
|
649
|
+
render(
|
|
650
|
+
<InputGroup.Root>
|
|
651
|
+
<Input value="test" />
|
|
652
|
+
<InputAddon>
|
|
653
|
+
<IconButton aria-label="Clear" onClick={handleClear}>
|
|
654
|
+
<XIcon />
|
|
655
|
+
</IconButton>
|
|
656
|
+
</InputAddon>
|
|
657
|
+
</InputGroup.Root>
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
const clearButton = screen.getByLabelText('Clear');
|
|
661
|
+
await userEvent.click(clearButton);
|
|
662
|
+
|
|
663
|
+
expect(handleClear).toHaveBeenCalledOnce();
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
test('addon icon is visible', () => {
|
|
667
|
+
render(
|
|
668
|
+
<InputGroup.Root>
|
|
669
|
+
<InputAddon>
|
|
670
|
+
<SearchIcon data-testid="search-icon" />
|
|
671
|
+
</InputAddon>
|
|
672
|
+
<Input />
|
|
673
|
+
</InputGroup.Root>
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
expect(screen.getByTestId('search-icon')).toBeInTheDocument();
|
|
677
|
+
});
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
## Related Components
|
|
681
|
+
|
|
682
|
+
- **InputGroup** - Required wrapper for positioning addons
|
|
683
|
+
- **Input** - Text input field that addons enhance
|
|
684
|
+
- **IconButton** - For interactive buttons within addons
|
|
685
|
+
- **Spinner** - For loading states in addons
|