ui icon indicating copy to clipboard operation
ui copied to clipboard

Accessiblity Problem With Scroll Area and Forms

Open nahasco opened this issue 1 year ago • 5 comments

Hi all. I have a simple form inside a dialog, to make sure the form fit the screen it should be scrollable so I decided to use the ScrollArea component and wrap around the form.

<ScrollArea type="always" className="h-[60vh]">
                <Form {...form}>
                    <form id="new_student" onSubmit={form.handleSubmit(onSubmit)} className="grid gap-y-5 py-4 pe-5">
                        <FormField
                            control={form.control}
                            name="first_name"
                            render={({ field }) => (
                                <FormItem className={formItemClass}>
                                    <FormLabel>الاسم</FormLabel>
                                    <div className={innerDivClass}>
                                        <FormControl>
                                            <Input tabIndex="0" {...field} />
                                        </FormControl>
                                        <FormMessage />
                                    </div>
                                </FormItem>
                            )}
                        />
                        <FormField
                            control={form.control}
                            name="last_name"
                            render={({ field }) => (
                                <FormItem className={formItemClass}>
                                    <FormLabel>اسم العائلة</FormLabel>
                                    <div className={innerDivClass}>
                                        <FormControl>
                                            <Input tabIndex="0" {...field} />
                                        </FormControl>
                                        <FormMessage />
                                    </div>
                                </FormItem>
                            )}
                        />
                        <FormField
                            control={form.control}
                            name="email"
                            render={({ field }) => (
                                <FormItem className={formItemClass}>
                                    <FormLabel>الايميل</FormLabel>
                                    <div className={innerDivClass}>
                                        <FormControl>
                                            <Input tabIndex="0" {...field} />
                                        </FormControl>
                                        <FormMessage />
                                    </div>
                                </FormItem>
                            )}
                        />

                    {/* other form fields */}

            </ScrollArea>

When I am on an input field and I try to tab to the next input, it randomly focuses on the dialog instead of the next input.

Here is a screen gif showcasing the issue: https://imgur.com/KYys2qZ

After hours of debugging, I discovered that the ScrollArea component is causing this odd behavior. I reproduced the issue using the library's own examples so you can have a look.

"use client"
import { Button } from "@/components/ui/button"
import {
    Dialog,
    DialogContent,
    DialogDescription,
    DialogFooter,
    DialogHeader,
    DialogTitle,
    DialogTrigger,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { useForm } from "react-hook-form"
import { ScrollArea } from "@/src/components/ui/scroll-area"

const formSchema = z.object({
    first: z.string().min(2, {
        message: "Username must be at least 2 characters.",
    }),
    second: z.string().min(2, {
        message: "Username must be at least 2 characters.",
    }),
    third: z.string().min(2, {
        message: "Username must be at least 2 characters.",
    }),
    fourth: z.string().min(2, {
        message: "Username must be at least 2 characters.",
    }),
    fifth: z.string().min(2, {
        message: "Username must be at least 2 characters.",
    }),
})

function TestComponent() {
    // 1. Define your form.
    const form = useForm({
        resolver: zodResolver(formSchema),
        defaultValues: {
            username: "",
        },
    })

    function onSubmit(values) {
        // Do something with the form values.
        // ✅ This will be type-safe and validated.
        console.log(values)
    }

    return (
        <>
            <Dialog>
                <DialogTrigger asChild>
                    <Button variant="outline">Edit Profile</Button>
                </DialogTrigger>
                <DialogContent className="sm:max-w-[425px]">
                    <DialogHeader>
                        <DialogTitle>Edit profile</DialogTitle>
                        <DialogDescription>
                            Make changes to your profile here. Click save when you're done.
                        </DialogDescription>
                    </DialogHeader>
                    {/* <ScrollArea className="h-[60vh]"> */}
                    <Form {...form}>
                        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
                            <FormField
                                control={form.control}
                                name="first"
                                render={({ field }) => (
                                    <FormItem>
                                        <FormLabel>Username</FormLabel>
                                        <FormControl>
                                            <Input placeholder="shadcn" {...field} />
                                        </FormControl>
                                        <FormDescription>This is your public display name.</FormDescription>
                                        <FormMessage />
                                    </FormItem>
                                )}
                            />
                            <FormField
                                control={form.control}
                                name="second"
                                render={({ field }) => (
                                    <FormItem>
                                        <FormLabel>Username</FormLabel>
                                        <FormControl>
                                            <Input placeholder="shadcn" {...field} />
                                        </FormControl>
                                        <FormDescription>This is your public display name.</FormDescription>
                                        <FormMessage />
                                    </FormItem>
                                )}
                            />
                            <FormField
                                control={form.control}
                                name="third"
                                render={({ field }) => (
                                    <FormItem>
                                        <FormLabel>Username</FormLabel>
                                        <FormControl>
                                            <Input placeholder="shadcn" {...field} />
                                        </FormControl>
                                        <FormDescription>This is your public display name.</FormDescription>
                                        <FormMessage />
                                    </FormItem>
                                )}
                            />
                            <FormField
                                control={form.control}
                                name="fourth"
                                render={({ field }) => (
                                    <FormItem>
                                        <FormLabel>Username</FormLabel>
                                        <FormControl>
                                            <Input placeholder="shadcn" {...field} />
                                        </FormControl>
                                        <FormDescription>This is your public display name.</FormDescription>
                                        <FormMessage />
                                    </FormItem>
                                )}
                            />
                            <FormField
                                control={form.control}
                                name="fifth"
                                render={({ field }) => (
                                    <FormItem>
                                        <FormLabel>Username</FormLabel>
                                        <FormControl>
                                            <Input placeholder="shadcn" {...field} />
                                        </FormControl>
                                        <FormDescription>This is your public display name.</FormDescription>
                                        <FormMessage />
                                    </FormItem>
                                )}
                            />
                            <Button type="submit">Submit</Button>
                        </form>
                    </Form>
                    {/* </ScrollArea> */}
                    <DialogFooter>
                        <Button type="submit">Save changes</Button>
                    </DialogFooter>
                </DialogContent>
            </Dialog>
        </>
    )
}

export default TestComponent

nahasco avatar Aug 19 '23 16:08 nahasco

I have the exact same issue with a form that has a scroll area inside. The focus while tabbing is lost to the dialog content.

SaizFerri avatar Nov 11 '23 19:11 SaizFerri

I've the same bug!

jovar-dev avatar Nov 20 '23 20:11 jovar-dev

As a temporary solution, add tabIndex={undefined} to the DialogContent or SheetContent component.

jovar-dev avatar Nov 20 '23 21:11 jovar-dev

@shadcn Hello, do you have any suggestions on how to fix this without the abIndex={undefined}?

kyrylolvov avatar Dec 31 '23 07:12 kyrylolvov

Other then issue with tab navigation, sometimes there is a bug when you need to click twice on next input, in order for it to focus.

kyrylolvov avatar Dec 31 '23 08:12 kyrylolvov

This issue has been automatically closed because it received no activity for a while. If you think it was closed by accident, please leave a comment. Thank you.

shadcn avatar Jun 08 '24 23:06 shadcn