@open-mercato/ui 0.6.8-develop.6998.1.d2bc46c56c → 0.6.8-develop.7003.1.24fef49d48

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.
@@ -5,6 +5,7 @@
5
5
  import type { LucideIcon } from 'lucide-react';
6
6
  import {
7
7
  Activity,
8
+ AlarmClock,
8
9
  AlertCircle,
9
10
  AlertOctagon,
10
11
  AlertTriangle,
@@ -44,6 +45,7 @@ import {
44
45
  Clock3,
45
46
  Coins,
46
47
  Copy,
48
+ CopyX,
47
49
  CreditCard,
48
50
  Crown,
49
51
  Database,
@@ -67,6 +69,7 @@ import {
67
69
  GitPullRequestArrow,
68
70
  Globe,
69
71
  GraduationCap,
72
+ Hammer,
70
73
  Hand,
71
74
  Handshake,
72
75
  Headphones,
@@ -82,6 +85,7 @@ import {
82
85
  LineChart,
83
86
  Link,
84
87
  List,
88
+ ListX,
85
89
  Loader,
86
90
  Loader2,
87
91
  Lock,
@@ -92,6 +96,7 @@ import {
92
96
  MapPinned,
93
97
  Megaphone,
94
98
  MessageCircle,
99
+ MessageSquareReply,
95
100
  MinusCircle,
96
101
  Notebook,
97
102
  Package,
@@ -109,6 +114,7 @@ import {
109
114
  ReceiptText,
110
115
  RefreshCcw,
111
116
  RefreshCw,
117
+ Repeat2,
112
118
  Reply,
113
119
  RotateCcw,
114
120
  Ruler,
@@ -156,10 +162,12 @@ import {
156
162
  Wrench,
157
163
  X,
158
164
  XCircle,
165
+ ZapOff,
159
166
  } from 'lucide-react';
160
167
 
161
168
  export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
162
169
  'activity': Activity,
170
+ 'alarm-clock': AlarmClock,
163
171
  'alert-circle': AlertCircle,
164
172
  'alert-octagon': AlertOctagon,
165
173
  'alert-triangle': AlertTriangle,
@@ -199,6 +207,7 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
199
207
  'clock-3': Clock3,
200
208
  'coins': Coins,
201
209
  'copy': Copy,
210
+ 'copy-x': CopyX,
202
211
  'credit-card': CreditCard,
203
212
  'crown': Crown,
204
213
  'database': Database,
@@ -222,6 +231,7 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
222
231
  'git-pull-request-arrow': GitPullRequestArrow,
223
232
  'globe': Globe,
224
233
  'graduation-cap': GraduationCap,
234
+ 'hammer': Hammer,
225
235
  'hand': Hand,
226
236
  'handshake': Handshake,
227
237
  'headphones': Headphones,
@@ -237,6 +247,7 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
237
247
  'line-chart': LineChart,
238
248
  'link': Link,
239
249
  'list': List,
250
+ 'list-x': ListX,
240
251
  'loader': Loader,
241
252
  'loader-2': Loader2,
242
253
  'lock': Lock,
@@ -247,6 +258,7 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
247
258
  'map-pinned': MapPinned,
248
259
  'megaphone': Megaphone,
249
260
  'message-circle': MessageCircle,
261
+ 'message-square-reply': MessageSquareReply,
250
262
  'minus-circle': MinusCircle,
251
263
  'notebook': Notebook,
252
264
  'package': Package,
@@ -264,6 +276,7 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
264
276
  'receipt-text': ReceiptText,
265
277
  'refresh-ccw': RefreshCcw,
266
278
  'refresh-cw': RefreshCw,
279
+ 'repeat-2': Repeat2,
267
280
  'reply': Reply,
268
281
  'rotate-ccw': RotateCcw,
269
282
  'ruler': Ruler,
@@ -311,4 +324,5 @@ export const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {
311
324
  'wrench': Wrench,
312
325
  'x': X,
313
326
  'x-circle': XCircle,
327
+ 'zap-off': ZapOff,
314
328
  };
@@ -82,6 +82,7 @@ export function ComboboxInput({
82
82
  const t = useT()
83
83
  const resolvedPlaceholder = placeholder ?? t('ui.inputs.comboboxInput.placeholder', 'Type to search...')
84
84
  const loadingLabel = t('ui.inputs.comboboxInput.loading', 'Loading suggestions…')
85
+ const noMatchesLabel = t('ui.inputs.comboboxInput.noMatches', 'No matches found')
85
86
  const resolvedClearLabel = clearLabel ?? t('ui.inputs.comboboxInput.clear', 'Clear value')
86
87
  const blurCloseDelayMs = 250
87
88
  const blurCloseMaxDelayMs = 1000
@@ -392,7 +393,9 @@ export function ComboboxInput({
392
393
  }, [optionDomId, selectedIndex, showSuggestions])
393
394
 
394
395
  const showClearButton = clearable && !disabled && (value !== '' || input !== '')
395
- const listboxVisible = showSuggestions && !disabled && (loading || filteredSuggestions.length > 0)
396
+ const listboxVisible = showSuggestions
397
+ && !disabled
398
+ && (loading || filteredSuggestions.length > 0 || (touched && input.trim().length > 0))
396
399
 
397
400
  return (
398
401
  <div className="relative w-full">
@@ -416,7 +419,7 @@ export function ComboboxInput({
416
419
  disabled={disabled}
417
420
  role="combobox"
418
421
  aria-expanded={listboxVisible}
419
- aria-controls={listboxId}
422
+ aria-controls={listboxVisible && !loading && filteredSuggestions.length > 0 ? listboxId : undefined}
420
423
  aria-autocomplete="list"
421
424
  aria-activedescendant={listboxVisible && selectedIndex >= 0 ? optionDomId(selectedIndex) : undefined}
422
425
  onFocus={() => {
@@ -467,9 +470,13 @@ export function ComboboxInput({
467
470
  ) : null}
468
471
 
469
472
  {listboxVisible && (
470
- <div className="absolute z-popover w-full mt-1 rounded-md border border-input bg-popover p-2 shadow-md max-h-48 sm:max-h-60 overflow-auto">
473
+ <div
474
+ className="absolute z-popover w-full mt-1 rounded-md border border-input bg-popover p-2 shadow-md max-h-48 sm:max-h-60 overflow-auto"
475
+ >
471
476
  {loading && touched ? (
472
- <div className="px-2 py-1.5 text-xs text-muted-foreground">{loadingLabel}</div>
477
+ <div className="px-2 py-1.5 text-xs text-muted-foreground" role="status">{loadingLabel}</div>
478
+ ) : touched && !filteredSuggestions.length ? (
479
+ <div className="px-2 py-1.5 text-xs text-muted-foreground" role="status">{noMatchesLabel}</div>
473
480
  ) : (
474
481
  <div id={listboxId} role="listbox" className="flex flex-col gap-1">
475
482
  {filteredSuggestions.map((option, index) => (
@@ -5,7 +5,7 @@ jest.mock('@open-mercato/shared/lib/i18n/context', () => ({
5
5
  }))
6
6
 
7
7
  import * as React from 'react'
8
- import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
8
+ import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
9
9
  import { ComboboxInput } from '../ComboboxInput'
10
10
 
11
11
  type HarnessProps = Partial<React.ComponentProps<typeof ComboboxInput>> & {
@@ -397,3 +397,23 @@ describe('ComboboxInput — eager label resolution', () => {
397
397
  await waitFor(() => expect(getInput(container).value).toBe('Label-b'))
398
398
  })
399
399
  })
400
+
401
+ describe('ComboboxInput accessibility', () => {
402
+ it('exposes status text separately and reserves listbox for options', () => {
403
+ render(<Harness />)
404
+ const input = screen.getByRole('combobox')
405
+
406
+ fireEvent.focus(input)
407
+ fireEvent.change(input, { target: { value: 'missing' } })
408
+
409
+ expect(screen.getByText(/no matches/i)).toHaveAttribute('role', 'status')
410
+ expect(screen.queryByRole('listbox')).toBeNull()
411
+ expect(input).not.toHaveAttribute('aria-controls')
412
+
413
+ fireEvent.change(input, { target: { value: 're' } })
414
+
415
+ const listbox = screen.getByRole('listbox')
416
+ expect(within(listbox).getAllByRole('option')).toHaveLength(2)
417
+ expect(input).toHaveAttribute('aria-controls', listbox.id)
418
+ })
419
+ })