Skip to content

Commit 3befafb

Browse files
committed
03/04: improve accessibility of the location suggestions listbox
1 parent a7dbf40 commit 3befafb

File tree

6 files changed

+45
-19
lines changed

6 files changed

+45
-19
lines changed

exercises/03.guides/04.problem.api-mocking/app/components/forms.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ import { Input } from './ui/input.tsx'
1212
import { Label } from './ui/label.tsx'
1313
import { Textarea } from './ui/textarea.tsx'
1414
import { cn } from '#app/utils/misc.tsx'
15-
import { Command, CommandItem, CommandList } from './ui/command.tsx'
15+
import {
16+
Command,
17+
CommandItem,
18+
CommandList,
19+
CommandListProps,
20+
} from './ui/command.tsx'
1621

1722
export type ListOfErrors = Array<string | null | undefined> | null | undefined
1823

@@ -206,14 +211,16 @@ export function CheckboxField({
206211
export function ComboboxField({
207212
labelProps,
208213
inputProps,
214+
listboxProps = {},
209215
errors,
210216
options,
211217
className,
212218
}: {
213219
labelProps: React.LabelHTMLAttributes<HTMLLabelElement>
214220
inputProps: React.TextareaHTMLAttributes<HTMLInputElement> & { key: any }
221+
listboxProps?: CommandListProps
215222
errors?: ListOfErrors
216-
options: Array<{ label: string; value: string }>
223+
options: Array<{ id: string | number; label: string; value: string }>
217224
className?: string
218225
}) {
219226
const fallbackId = useId()
@@ -228,8 +235,9 @@ export function ComboboxField({
228235
const [isDirty, setDirty] = useState(false)
229236
const [isListOpen, setListOpen] = useState(false)
230237
const [query, setQuery] = useState<string>('')
238+
231239
const [filtered, setFiltered] = useState<
232-
Array<{ label: string; value: string }>
240+
Array<{ id: string | number; label: string; value: string }>
233241
>([])
234242

235243
const filterOptions = useCallback(
@@ -257,6 +265,8 @@ export function ComboboxField({
257265
<Label htmlFor={id} {...labelProps} />
258266
<Input
259267
{...inputProps}
268+
role="combobox"
269+
aria-expanded={isListOpen ? 'true' : 'false'}
260270
aria-invalid={errorId ? true : undefined}
261271
aria-describedby={errorId}
262272
autoComplete="off"
@@ -281,10 +291,10 @@ export function ComboboxField({
281291
{isListOpen && filtered.length > 0 && (
282292
<div className="bg-popover text-popover-foreground border-muted-foreground/60 absolute z-10 mt-1 w-full rounded-md border shadow">
283293
<Command>
284-
<CommandList>
294+
<CommandList {...listboxProps}>
285295
{filtered.map((option) => (
286296
<CommandItem
287-
key={option.value}
297+
key={option.id}
288298
value={option.value}
289299
onSelect={() => {
290300
control.change(option.value)

exercises/03.guides/04.problem.api-mocking/app/components/ui/command.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,11 @@ function CommandInput({
8080
)
8181
}
8282

83-
function CommandList({
84-
className,
85-
...props
86-
}: React.ComponentProps<typeof CommandPrimitive.List>) {
83+
export type CommandListProps = React.ComponentProps<
84+
typeof CommandPrimitive.List
85+
>
86+
87+
function CommandList({ className, ...props }: CommandListProps) {
8788
return (
8889
<CommandPrimitive.List
8990
data-slot="command-list"

exercises/03.guides/04.solution.api-mocking/app/components/forms.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ import { Input } from './ui/input.tsx'
1212
import { Label } from './ui/label.tsx'
1313
import { Textarea } from './ui/textarea.tsx'
1414
import { cn } from '#app/utils/misc.tsx'
15-
import { Command, CommandItem, CommandList } from './ui/command.tsx'
15+
import {
16+
Command,
17+
CommandItem,
18+
CommandList,
19+
CommandListProps,
20+
} from './ui/command.tsx'
1621

1722
export type ListOfErrors = Array<string | null | undefined> | null | undefined
1823

@@ -206,12 +211,14 @@ export function CheckboxField({
206211
export function ComboboxField({
207212
labelProps,
208213
inputProps,
214+
listboxProps = {},
209215
errors,
210216
options,
211217
className,
212218
}: {
213219
labelProps: React.LabelHTMLAttributes<HTMLLabelElement>
214220
inputProps: React.TextareaHTMLAttributes<HTMLInputElement> & { key: any }
221+
listboxProps?: CommandListProps
215222
errors?: ListOfErrors
216223
options: Array<{ id: string | number; label: string; value: string }>
217224
className?: string
@@ -258,6 +265,8 @@ export function ComboboxField({
258265
<Label htmlFor={id} {...labelProps} />
259266
<Input
260267
{...inputProps}
268+
role="combobox"
269+
aria-expanded={isListOpen ? 'true' : 'false'}
261270
aria-invalid={errorId ? true : undefined}
262271
aria-describedby={errorId}
263272
autoComplete="off"
@@ -282,7 +291,7 @@ export function ComboboxField({
282291
{isListOpen && filtered.length > 0 && (
283292
<div className="bg-popover text-popover-foreground border-muted-foreground/60 absolute z-10 mt-1 w-full rounded-md border shadow">
284293
<Command>
285-
<CommandList>
294+
<CommandList {...listboxProps}>
286295
{filtered.map((option) => (
287296
<CommandItem
288297
key={option.id}

exercises/03.guides/04.solution.api-mocking/app/components/ui/command.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,11 @@ function CommandInput({
8080
)
8181
}
8282

83-
function CommandList({
84-
className,
85-
...props
86-
}: React.ComponentProps<typeof CommandPrimitive.List>) {
83+
export type CommandListProps = React.ComponentProps<
84+
typeof CommandPrimitive.List
85+
>
86+
87+
function CommandList({ className, ...props }: CommandListProps) {
8788
return (
8889
<CommandPrimitive.List
8990
data-slot="command-list"

exercises/03.guides/04.solution.api-mocking/app/routes/users+/$username_+/__note-editor.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ export function NoteEditor({
155155
...getInputProps(fields.location, { type: 'text' }),
156156
placeholder: 'Select a location...',
157157
}}
158+
listboxProps={{
159+
label: 'Location suggestions',
160+
'aria-label': 'Location suggestions',
161+
}}
158162
errors={fields.location.errors}
159163
/>
160164
<div>

exercises/03.guides/04.solution.api-mocking/tests/e2e/notes-create.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ test('displays location suggestions when creating a new note', async ({
3737

3838
const locationInput = page.getByLabel('Location')
3939
await locationInput.fill('San')
40-
await expect(page.getByRole('option')).toHaveText([
41-
'San Francisco',
42-
'San Jose',
43-
])
40+
await expect(
41+
page
42+
.getByRole('listbox', { name: 'Location suggestions' })
43+
.getByRole('option'),
44+
).toHaveText(['San Francisco', 'San Jose'])
4445

4546
await page.getByRole('option', { name: 'San Francisco' }).click()
4647
await expect(locationInput).toHaveValue('San Francisco')

0 commit comments

Comments
 (0)