skip to Main Content

Basically I want to create 3 modals stacked on top of one another, like the image below.
Clicking on the Create Ecosytem will open another shadcn Dialog like 2nd image below

enter image description here

enter image description here

The problem I am facing it is to effectively stack it one above the other
So there are 3 Modals,

  1. Search Modal
  2. Watchlist Modal
  3. Add Ecosystem Modal
  • When there is only 1 modal (Search Modal) , clicking outside should close the modal
  • When there is 2 modal (WatchList Modal and Search Modal), clicking outside should first close the Watchlist Modal(Top in the stack), and then clicking again outside should close the Search Modal
  • Similar story with the Add Ecosystem Modal, that it needs to be properly stacked,

Like Search Modal -> Watchlist Modal -> Add Ecosystem Modal.

Using the present onFocus onBlur is really ineffective, so How can i proceed?

p.s: I am open to using modal/dialog from material/ui and shadcn, positioning is my concern…

Code snippets below, full code link at the very bottom

SearchBar.tsx

"use client";
import { useEffect, useState } from "react";
import SearchResult from "./SearchResult";

export default function SearchBar() {
  const [results, setResults] = useState([]);
  const [isVisible, setIsVisible] = useState(false);

  const stockMapping = [
    {
      StockId: 2,
      StockName: "Tata Consultancy Services Limited",
      Ecosystem: [
        {
          EcosystemName: "Eco1",
          EcosystemId: 21,
        },
        {
          EcosystemName: "Eco2",
          EcosystemId: 22,
        },
      ],
    },
    {
      StockId: 4,
      StockName: "ICICI Bank Limited",
      Ecosystem: [
        {
          EcosystemName: "ddfs",
          EcosystemId: 13,
        },
      ],
    },
    {
      StockId: 1,
      StockName: "Reliance Industries Limited",
      Ecosystem: [],
    },
    {
      StockId: 5,
      StockName: "State Bank of India",
      Ecosystem: [],
    },
  ];

  const [searchInput, setSearchInput] = useState("");
  const handleChange = async (value) => {
    setSearchInput(value);
  };

  useEffect(() => {
    const filteredStocks = stockMapping.filter((stock) =>
      stock.StockName.toLowerCase().includes(searchInput.toLowerCase())
    );
    setResults(filteredStocks);
  }, [searchInput]);

  const handleFocus = () => {
    setIsVisible(true);
  };

  const handleBlur = () => {
    // Adding a small delay to allow time for the click event on the results to trigger before hiding them
    setTimeout(() => {
      setIsVisible(false);
    }, 100);
  };

  return (
    <div className="bg-gray-200 flex w-[600px] mr-32 ">
      <div className=" bg-white rounded-md  w-full relative z-0">
        <div className="flex items-center border rounded-md h-full px-4">
          <input
            placeholder="Search for stocks"
            value={searchInput}
            onChange={(e) => handleChange(e.target.value)}
            className="bg-transparent border-none h-full ml-2 w-full focus:outline-none"
            onFocus={handleFocus}
            onBlur={handleBlur}
          />
        </div>
        {results && results.length > 0 && (
          <div
            className={`inset w-full absolute inset-x-0 z-10 top-8 rounded-md bg-white shadow-md mt-4 max-h-300 overflow-y-auto ${
              isVisible ? `block` : `hidden`
            }`}
          >
            {results
              ? results
                  .slice(0, 5)
                  .map((result, id) => (
                    <SearchResult result={result} key={id} />
                  ))
              : isVisible && <div>No results found</div>}
          </div>
        )}
      </div>
    </div>
  );
}

SearchResult

const SearchResult = ({ result }) => {
  return (
    <div className="search-result p-2 ml-5 flex justify-between hover:bg-gray-300 ">
      {result.StockName}
      {result.Ecosystem.length > 0 ? (
        <div className="cursor-pointer">Added</div>
      ) : (
        <div className="cursor-pointer">Add now</div>
      )}
    </div>
  );
};

export default SearchResult;

Get the full code link here: https://github.com/krishnaacharyaa/search-nextjs-modal

2

Answers


  1. One approach would be to have each modal actually be two divs: one "background" div that if clicked, closes the modal, and one actual div for the modal content.

    In the image below, the red would be your main page.

    The first modal would consist of a div that is the size of the red rectangle where if you click it, the modal will close; and the actual modal content would be the the orange rectangle.

    The second modal would consist of a div that is the size of the red rectangle where if you click it, the second modal will close; and the actual modal content would be the the yellow rectangle.

    The third modal would consist of a div that is the size of the red rectangle where if you click it, the third modal will close; and the actual modal content would be the the green rectangle.

    This will also allow you to set a background color and opacity on the "background" divs to gray out the stuff behind the modal if you please.

    Also, consider making each modal a parallel route. This is the standard way modals are done in next.js as it allows you to have them all as server components as you are not using react state to handle open/close

    modals

    Login or Signup to reply.
  2. The idea mentioned by Makazau is correct. RadixUI, the headless UI powering shadcn, includes a <Dialog.Overlay> component that renders a <div>; clicking on it will close the dialog.

    In shadcn, this is exported as DialogOverlay with styles that cover the entire screen, defined as fixed inset-0 z-50 bg-black/80. It is internally utilized by the <DialogContent> component.

    To implement your case, you just need to render <DialogOverlay /> in every nested dialog.

    <Dialog>
        <DialogTrigger>Trigger</DialogTrigger>
        {/* Internally <DialogContent> renders a <DialogOverlay> */}
        <DialogContent>
            <DialogHeader>
                <DialogTitle>Dialog 1</DialogTitle>
            </DialogHeader>
            <div>
                Outer Dialog Content
            </div>
            <DialogFooter>
                {/* Nested Dialog */}
                <Dialog>
                    <DialogTrigger>Nested Trigger</DialogTrigger>
                    <DialogContent>
                        <DialogHeader>
                            <DialogTitle>Dialog 2</DialogTitle>
                        </DialogHeader>
                        <div>
                            Nested Dialog Content
                        </div>
                        <DialogFooter>
                            <DialogClose>Close Nested</DialogClose>
                        </DialogFooter>
                    </DialogContent>
                </Dialog>
                <DialogClose>Close</DialogClose>
            </DialogFooter>
        </DialogContent>
    </Dialog>
    

    Where <DialogContent> renders a new <Portal> for each dialog, and its own <Overlay>

    
    const DialogContent = React.forwardRef<
      React.ElementRef<typeof DialogPrimitive.Content>,
      React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
    >(({ className, children, ...props }, ref) => (
      <DialogPortal>
        <DialogOverlay />
        <DialogPrimitive.Content
          ref={ref}
          className={cn(
            "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
            className
          )}
          {...props}
        >
          {children}
          <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
            <X className="h-4 w-4" />
            <span className="sr-only">Close</span>
          </DialogPrimitive.Close>
        </DialogPrimitive.Content>
      </DialogPortal>
    ))
    DialogContent.displayName = DialogPrimitive.Content.displayName
    

    A working example of your design should look like this

    npx shadcn-ui add dialog command input button
    
    "use client";
    import { useMemo } from "react";
    import {
      Command,
      CommandInput,
      CommandItem,
      CommandList,
    } from "@/components/ui/command";
    import { Button } from "@/components/ui/button";
    import {
      Dialog,
      DialogClose,
      DialogContent,
      DialogFooter,
      DialogHeader,
      DialogTrigger,
    } from "@/components/ui/dialog";
    import { DialogTitle } from "@radix-ui/react-dialog";
    import { Input } from "@/components/ui/input";
    
    export default function SearchBar() {
      const stockMapping = useMemo(
        () => [
          {
            StockId: 2,
            StockName: "Tata Consultancy Services Limited",
            Ecosystem: [
              {
                EcosystemName: "Eco1",
                EcosystemId: 21,
              },
              {
                EcosystemName: "Eco2",
                EcosystemId: 22,
              },
            ],
          },
          {
            StockId: 4,
            StockName: "ICICI Bank Limited",
            Ecosystem: [
              {
                EcosystemName: "ddfs",
                EcosystemId: 13,
              },
            ],
          },
          {
            StockId: 1,
            StockName: "Reliance Industries Limited",
            Ecosystem: [],
          },
          {
            StockId: 5,
            StockName: "State Bank of India",
            Ecosystem: [],
          },
        ],
        []
      );
    
      return (
        <Command className="bg-gray-200 flex w-[600px] mr-32 ">
          <CommandInput placeholder="Search for stocks" />
          <CommandList>
            {stockMapping.map((stock) => (
              <CommandItem
                key={stock.StockId}
                value={`${stock.StockName} ${stock.Ecosystem.map(
                  (eco) => eco.EcosystemName
                ).join(" ")}`}
                className="justify-between"
              >
                <span>{stock.StockName}</span>
                <Dialog>
                  <DialogTrigger asChild>
                    <Button size="sm">Bookmark</Button>
                  </DialogTrigger>
                  <DialogContent>
                    <DialogHeader>
                      <DialogTitle>Update Stock to</DialogTitle>
                    </DialogHeader>
                    <div>
                      <ul className="p-4 space-y-1">
                        <li className="py-2 px-4 cursor-pointer rounded hover:bg-slate-200">
                          Bancking
                        </li>
                        <li className="py-2 px-4 cursor-pointer rounded hover:bg-slate-200">
                          FMCO
                        </li>
                        <li className="py-2 px-4 cursor-pointer rounded hover:bg-slate-200">
                          Whatever
                        </li>
                      </ul>
                    </div>
                    <DialogFooter>
                      <Dialog>
                        <DialogTrigger asChild>
                          <Button>Create New Stock</Button>
                        </DialogTrigger>
                        <DialogContent>
                          <DialogHeader>
                            <DialogTitle>Create New Stock</DialogTitle>
                          </DialogHeader>
                          <div>
                            <Input placeholder="Stock Name" />
                          </div>
                          <DialogFooter>
                            <Dialog>
                              <DialogTrigger asChild>
                                <Button>One More nesting</Button>
                              </DialogTrigger>
                              <DialogContent>
                                <DialogHeader>
                                  <DialogTitle>Nesting</DialogTitle>
                                </DialogHeader>
                                <DialogFooter>
                                  <DialogClose>
                                    <Button variant="secondary">Cancel</Button>
                                  </DialogClose>
                                  <DialogClose asChild>
                                    <Button>Save</Button>
                                  </DialogClose>
                                </DialogFooter>
                              </DialogContent>
                            </Dialog>
                            <DialogClose asChild>
                              <Button>Save</Button>
                            </DialogClose>
                          </DialogFooter>
                        </DialogContent>
                      </Dialog>
                      <DialogClose asChild>
                        <Button>Save</Button>
                      </DialogClose>
                    </DialogFooter>
                  </DialogContent>
                </Dialog>
              </CommandItem>
            ))}
          </CommandList>
        </Command>
      );
    }
    
    

    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search