Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[bug]: Radix UI Dialog & Popover Block Body Scroll (Mouse Wheel Disabled) #6988

Open
2 tasks done
jsdev-robin opened this issue Mar 21, 2025 · 2 comments
Open
2 tasks done
Labels
bug Something isn't working

Comments

@jsdev-robin
Copy link

jsdev-robin commented Mar 21, 2025

Describe the bug

Project Link: https://mun-xi.vercel.app/seller/dashboard/discounts

When opening a Radix UI or , the body scroll (mouse wheel scrolling) is completely blocked. Even when modal={false} is set for the , or forceMount is used in the , the issue persists. This prevents users from scrolling the background content while these components are open.

Image

Affected component/components

"use client";

import React, { useEffect } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import {
  CalendarIcon,
  Check,
  CheckIcon,
  ChevronsUpDown,
  Plus,
  RefreshCcw,
  X,
} from "lucide-react";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";

import { generateCouponCode } from "@/lib/generateCoupon";
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip";
import {
  Command,
  CommandGroup,
  CommandItem,
  CommandList,
} from "@/components/ui/command";
import { useFormatOptions } from "@/hooks/useFormatOptions";
import { dummyProducts } from "@/data/dummyProducts";
import { Badge } from "@/components/ui/badge";
import { useSelection } from "@/hooks/use-selection";
import { Calendar } from "@/components/ui/calendar";
import { cn } from "@/lib/utils";
import { format } from "date-fns";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";

const formSchema = z.object({
  name: z.string().min(1, "Name is required"),
  products: z
    .array(
      z.object({
        value: z.string().min(1, "Product value is required"),
        label: z.string().min(1, "Product label is required"),
      })
    )
    .min(1, "At least one product is required"),
  code: z.string().min(1, "Code is required"),
  discount: z.object({
    type: z
      .string({ required_error: "Discount type is required" })
      .refine((val) => ["cash", "percent"].includes(val), {
        message: "Discount type must be 'cash' or 'percent'",
      }),
    value: z
      .string()
      .min(1, "Discount value is required")
      .regex(/^\d+$/, "Discount value must be a valid number"),
  }),
  startDate: z.date({ required_error: "A start date is required." }),
  endDate: z.date({ required_error: "An end date is required." }),
  redemptionLimit: z.string().optional(),
});

const discountTypes = [
  {
    value: "cash",
    label: "$",
  },
  {
    value: "percent",
    label: "%",
  },
];

const DiscountCreateForm = () => {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    mode: "onChange",
    defaultValues: {
      name: "",
      products: [
        {
          value: "",
          label: "",
        },
      ],
      code: "",
      discount: {
        type: "cash",
        value: "",
      },
      startDate: new Date(),
      endDate: undefined,
      redemptionLimit: "",
    },
  });

  function onSubmit(data: z.infer<typeof formSchema>) {
    console.log(data);
  }

  const [open, setOpen] = React.useState(false);
  const options = useFormatOptions({
    data: dummyProducts,
    value: "barcode",
    label: "item",
    options: { value: "all", label: "All" },
  });
  const { value, onSelect, onRemove, shouldSelect } = useSelection(options);

  useEffect(() => {
    if (value) {
      form.setValue("products", value);
    }
  }, [form, value]);

  const productImage = (barcode: string) =>
    dummyProducts.find((product) => product.barcode === barcode);

  useEffect(() => {
    const handleScrollUnlock = () => {
      document.body.style.overflow = "auto"; // Allow scrolling
    };

    handleScrollUnlock();
    return () => {
      document.body.style.overflow = ""; // Restore when closing
    };
  }, []);

  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button size="sm">
          <Plus />
          New discount
        </Button>
      </DialogTrigger>
      <DialogContent className="w-[90vw] max-w-[500px] p-0 gap-0">
        <Form {...form}>
          <form onSubmit={form.handleSubmit(onSubmit)}>
            <DialogHeader className="p-4 border-b">
              <DialogTitle>Add discount</DialogTitle>
              <DialogDescription />
            </DialogHeader>
            <div className="p-4 whisper-scroll max-h-[calc(100vh-150px)]">
              <div className="space-y-4">
                <FormField
                  control={form.control}
                  name="name"
                  render={({ field }) => (
                    <FormItem>
                      <FormLabel>Name</FormLabel>
                      <FormControl>
                        <Input {...field} />
                      </FormControl>
                      <FormDescription>
                        This will appear on your customer’s invoice.
                      </FormDescription>
                      <FormMessage />
                    </FormItem>
                  )}
                />

                <FormField
                  control={form.control}
                  name="products"
                  render={({ field }) => (
                    <FormItem>
                      <FormLabel>Products</FormLabel>
                      <Popover>
                        <PopoverTrigger asChild>
                          <Button
                            variant="outline"
                            role="combobox"
                            aria-expanded={open}
                            className="w-full justify-start h-full flex-wrap"
                          >
                            {field.value.length > 0 ? (
                              <>
                                {value.slice(0, 5).map((option) => (
                                  <Badge
                                    variant="outline"
                                    key={option.value}
                                    className="flex items-center gap-1"
                                  >
                                    <Avatar className="size-4">
                                      <AvatarImage
                                        src={
                                          productImage(option.value)?.img.src
                                        }
                                      />
                                      <AvatarFallback>
                                        {productImage(option.value)?.item[0]}
                                      </AvatarFallback>
                                    </Avatar>
                                    {option.label}
                                    <span
                                      className="cursor-pointer hover:text-red-500"
                                      onClick={(e) => {
                                        e.stopPropagation();
                                        onRemove(option.value);
                                      }}
                                    >
                                      <X />
                                    </span>
                                  </Badge>
                                ))}
                                {value.length > 5 && (
                                  <Badge
                                    variant="outline"
                                    className="font-semibold"
                                  >
                                    +{value.length - 5} more
                                  </Badge>
                                )}
                              </>
                            ) : (
                              <div className="text-muted-foreground">
                                Select options (multi-select)...
                              </div>
                            )}

                            <ChevronsUpDown className="text-muted-foreground ml-auto" />
                          </Button>
                        </PopoverTrigger>
                        <PopoverContent
                          className="w-(--radix-popover-trigger-width) p-0"
                          align="start"
                        >
                          <Command>
                            <CommandList>
                              <CommandGroup>
                                {options.map((option) => (
                                  <CommandItem
                                    key={option.value}
                                    value={option.value}
                                    onSelect={() => onSelect(option.value)}
                                  >
                                    <div
                                      className="border-input data-[selected=true]:border-primary data-[selected=true]:bg-primary data-[selected=true]:text-primary-foreground pointer-events-none size-4 shrink-0 rounded-[4px] border transition-all select-none *:[svg]:opacity-0 data-[selected=true]:*:[svg]:opacity-100"
                                      data-selected={shouldSelect(option.value)}
                                    >
                                      <CheckIcon className="size-3.5 text-current" />
                                    </div>

                                    <Avatar className="size-5">
                                      <AvatarImage
                                        src={
                                          productImage(option.value)?.img.src
                                        }
                                      />
                                      <AvatarFallback>
                                        {productImage(option.value)?.item[0]}
                                      </AvatarFallback>
                                    </Avatar>

                                    {option.label}
                                  </CommandItem>
                                ))}
                              </CommandGroup>
                            </CommandList>
                          </Command>
                        </PopoverContent>
                      </Popover>
                      <FormDescription>
                        Select the products this discount applies to.
                      </FormDescription>
                      <FormMessage />
                    </FormItem>
                  )}
                />

                <FormField
                  control={form.control}
                  name="code"
                  render={({ field }) => (
                    <FormItem>
                      <FormLabel>Code</FormLabel>
                      <div className="relative">
                        <Tooltip>
                          <TooltipTrigger
                            className="cursor-pointer absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
                            onClick={(e) => {
                              e.preventDefault();
                              form.setValue("code", generateCouponCode());
                            }}
                            type="button"
                          >
                            <RefreshCcw size={16} />
                          </TooltipTrigger>
                          <TooltipContent>Generate code</TooltipContent>
                        </Tooltip>

                        <FormControl>
                          <Input {...field} className="pr-8" />
                        </FormControl>
                      </div>
                      <FormDescription>
                        The code your customers will enter during checkout.
                      </FormDescription>
                      <FormMessage />
                    </FormItem>
                  )}
                />
                <FormField
                  control={form.control}
                  name="discount.value"
                  render={({ field }) => (
                    <FormItem>
                      <FormLabel>Amount</FormLabel>
                      <div className="relative">
                        <Popover open={open} onOpenChange={setOpen}>
                          <PopoverTrigger asChild>
                            <Button
                              variant="ghost"
                              role="combobox"
                              aria-expanded={open}
                              size="sm"
                              className="absolute right-0.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground cursor-pointer"
                              title="Click to switch the discount type (Cash or Percent)"
                            >
                              {form.getValues("discount.type") &&
                                discountTypes.find(
                                  (item) =>
                                    item.value ===
                                    form.getValues("discount.type")
                                )?.label}
                            </Button>
                          </PopoverTrigger>
                          <PopoverContent align="end" className="w-16 p-0">
                            <Command>
                              <CommandList>
                                <CommandGroup>
                                  {discountTypes.map((item) => (
                                    <CommandItem
                                      key={item.value}
                                      value={item.value}
                                      onSelect={() => {
                                        form.setValue(
                                          "discount.type",
                                          item.value
                                        );
                                        setOpen(false);
                                      }}
                                    >
                                      {item.label}
                                      <Check
                                        size={16}
                                        className={cn(
                                          "mr-0",
                                          form.getValues("discount.type") ===
                                            item.value
                                            ? "opacity-100"
                                            : "opacity-0"
                                        )}
                                      />
                                    </CommandItem>
                                  ))}
                                </CommandGroup>
                              </CommandList>
                            </Command>
                          </PopoverContent>
                        </Popover>
                        <FormControl>
                          <Input {...field} className="pr-8" />
                        </FormControl>
                      </div>
                      <FormMessage />
                    </FormItem>
                  )}
                />
                <div className="grid gap-4 md:grid-cols-2">
                  <FormField
                    control={form.control}
                    name="startDate"
                    render={({ field }) => (
                      <FormItem className="flex flex-col">
                        <FormLabel>Date of birth</FormLabel>
                        <Popover>
                          <PopoverTrigger asChild>
                            <FormControl>
                              <Button
                                variant={"outline"}
                                className={cn(
                                  "w-full pl-3 text-left font-normal",
                                  !field.value && "text-muted-foreground"
                                )}
                              >
                                {field.value ? (
                                  format(field.value, "PPP")
                                ) : (
                                  <span>Pick a date</span>
                                )}
                                <CalendarIcon
                                  size={16}
                                  className="ml-auto opacity-50"
                                />
                              </Button>
                            </FormControl>
                          </PopoverTrigger>
                          <PopoverContent className="w-auto p-0" align="start">
                            <Calendar
                              mode="single"
                              selected={field.value}
                              onSelect={field.onChange}
                              disabled={(date) =>
                                date < new Date(new Date().setHours(0, 0, 0, 0))
                              }
                            />
                          </PopoverContent>
                        </Popover>
                        <FormMessage />
                      </FormItem>
                    )}
                  />
                  <FormField
                    control={form.control}
                    name="endDate"
                    render={({ field }) => (
                      <FormItem className="flex flex-col">
                        <FormLabel>End date</FormLabel>
                        <Popover>
                          <PopoverTrigger asChild>
                            <FormControl>
                              <Button
                                variant="outline"
                                className={cn(
                                  "w-full pl-3 text-left font-normal",
                                  !field.value && "text-muted-foreground"
                                )}
                              >
                                {field.value ? (
                                  format(field.value, "PPP")
                                ) : (
                                  <span>Pick a date</span>
                                )}
                                <CalendarIcon
                                  size={16}
                                  className="ml-auto opacity-50"
                                />
                              </Button>
                            </FormControl>
                          </PopoverTrigger>
                          <PopoverContent className="w-auto p-0" align="start">
                            <Calendar
                              mode="single"
                              selected={field.value}
                              onSelect={field.onChange}
                            />
                          </PopoverContent>
                        </Popover>
                        <FormMessage />
                      </FormItem>
                    )}
                  />
                </div>

                <FormField
                  control={form.control}
                  name="redemptionLimit"
                  render={({ field }) => (
                    <FormItem>
                      <FormLabel>Limit the number of redemptions?</FormLabel>
                      <FormControl>
                        <Input type="number" {...field} />
                      </FormControl>
                      <FormDescription>
                        Limit applies across all customers, not per customer.
                      </FormDescription>
                      <FormMessage />
                    </FormItem>
                  )}
                />
              </div>
            </div>
            <DialogFooter className="p-4 pt-0">
              <Button type="submit">Save changes</Button>
            </DialogFooter>
          </form>
        </Form>
      </DialogContent>
    </Dialog>
  );
};

export default DiscountCreateForm;

How to reproduce

Description

When opening a Radix UI <Dialog> or <Popover>, the body scroll gets locked, preventing users from scrolling the page with the mouse wheel. This behavior is problematic for pages with long content.

Steps to Reproduce

  1. Open a Radix UI <Dialog> or <Popover>.
  2. Try to scroll the background using the mouse wheel.
  3. Notice that the body scroll is locked, preventing page navigation.

Expected Behavior

  • The body should remain scrollable when the <Dialog> or <Popover> is open.
  • The user should be able to scroll the page content with the mouse wheel while interacting with these components.

Possible Fixes

  • Manually override document.body.style.overflow when the component is opened.
  • Use the body-scroll-lock package to prevent unintended scroll locking while maintaining focus trapping.
  • Check if Radix UI has built-in options to disable body scroll locking.

Environment

  • Library: Radix UI (@radix-ui/react-dialog, @radix-ui/react-popover)
  • Framework: React / Next.js
  • Browser: All modern browsers
  • OS: Windows / macOS / Linux

Additional Context

This issue affects usability, especially for users who need to interact with content outside the modal while keeping it open.

Codesandbox/StackBlitz link

No response

Logs

System Info

## System Info
- **OS**: Windows / macOS / Linux  
- **Browser**: Chrome, Firefox, Edge, Safari  
- **Framework**: React / Next.js  
- **Radix UI Version**: [Specify version]  
- **Node.js Version**: [Specify version]  
- **Package Manager**: npm / yarn / pnpm (Specify version)

Before submitting

  • I've made research efforts and searched the documentation
  • I've searched for existing issues
@jsdev-robin jsdev-robin added the bug Something isn't working label Mar 21, 2025
@BenoitBousselot
Copy link

I have exactly the same issue in component like this : https://shadcn-country-dropdown.vercel.app/ inside Sheet component.

@Woofer21
Copy link

Woofer21 commented Mar 23, 2025

I have exactly the same issue in component like this : https://shadcn-country-dropdown.vercel.app/ inside Sheet component.

Strangely enough your component linked there scrolls fine for me.

Edit: It is because i had it in a dialog, it is fixed if you add modal={true} to the Popover element.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants