Search code examples
javascriptreactjsfluent-uifluentui-react

Can I render a React component after it returns?


I have a React component like this

export const NewComponent: React.FC<NewComponentProps> = ({prop1, prop2, prop3 }) => {
    const getAbsPositionDialog = () => {
        const element = document.getElementById('modalDialogId');
        if (!element) {
            return { x: 0, y: 0 };
        }
        const dialogPosition = element.getBoundingClientRect();
        return { x: dialogPosition.x, y: dialogPosition.y };
    }

    const dialogPosition = getAbsPositionDialog();
    const horizontal = dialogPosition.x + 100;
    const vertical = dialogPosition.y + 100;

    const toasterId = useId("toaster");
    const { dispatchToast } = useToastController(toasterId);
    const notify = (title: string) => {
        dispatchToast(
            <Toast>
              <ToastTitle>{title}</ToastTitle>
            </Toast>,
            { intent: "success" }
          );
    }

    React.useEffect(() => {
        <Toaster
            toasterId={toasterId}
            position="top-start"
            offset={{horizontal: horizontal, vertical: vertical}}
            pauseOnHover
            pauseOnWindowBlur
            timeout={1000}
        />
    }, [horizontal, vertical]);

    return (
        <div>
            <Button onClick={notify("Copied!")}/>
        </div>
    );
};

The idea is to show a Toast component when clicking on the button. This NewComponent is shown within a modal dialog and the location of the Toast notification is set at the top-start relative to the whole window screen size, so it works well like that, showing the Toast notification at that position. No problem with it.

The problem comes when I try to add some offset based on the modalDialogId. I want to know where the dialog is located so I can move the Toast notification closer to it, but when the component is executed, the modal dialog is not loaded yet and therefore the offset always returns as 0, 0.

I tried to use the React.useEffect to render the Toaster which is where I set my offset later, but that didn't seem to work.

Is there a way to achieve this?


Solution

  • for react-toaster you can set toaster postion style to absolute :

    containerStyle= {{ position : "absolute"}}
    

    and set the modal container position to relative

    position: "relative",
    

    demo :

    import toast, { Toaster } from "react-hot-toast";
    import Modal from "react-modal";
    
    const dismissToast = () => toast.remove();
    
    const successToast = () => toast.success("Here is your toast.");
    
    export default function App() {
      const customStyles = {
        content: {
          top: "20%",
          left: "20%",
          right: "20%",
          bottom: "20%",
          background: "#e5e5e5"
        }
      };
    
      return (
        <div>
          <Modal
            style={customStyles}
            isOpen={true}
            contentLabel="Minimal Modal Example"
          >
            <div style={{ position: "relative", margin: "auto", height: "100%" }}>
              <button onClick={successToast}>Success toast</button>
              <button onClick={dismissToast}>Remove all toasts</button>
              <hr />
              <h1>i am modal</h1>
              <Toaster
                position="bottom-left"
                containerClassName="fff"
                containerStyle={{ position: "absolute", inset: 0 }}
                toastOptions={{
                  duration: 3000000,
                  iconTheme: {
                    primary: "red",
                    secondary: "white"
                  },
                  role: "status",
                  ariaLive: "polite",
                  style: {
                    background: "green",
                    color: "whitesmoke"
                  }
                }}
              />
            </div>
          </Modal>
        </div>
      );
    }
    

    and the toast will be shown inside the modal without any extra code

    enter image description here

    for fluterui toaster you need to use callback ref to the modal and you need to add observer when the modal resized (note i use react-modal for the demo and you can create custom hook if you need):

    import * as React from "react";
    import { useState } from "react";
    import {
      useId,
      Button,
      Toaster,
      useToastController,
      ToastTitle,
      Toast
    } from "@fluentui/react-components";
    import Modal from "react-modal";
    
    export const DefaultToastOptions = () => {
      const [x, setX] = useState(0);
      const [y, setY] = useState(0);
      const [isOpen, setIsOpen] = useState(false);
    
      const toasterId = useId("toaster");
      const { dispatchToast } = useToastController(toasterId);
    
      const ref = React.useRef(null);
    
      const dialogdRef = React.useCallback((node) => {
        if (node !== null) {
          ref.current = node;
          const dialogPosition = ref.current.getBoundingClientRect();
    
          setX(dialogPosition.x + 10);
          setY(dialogPosition.y - 10);
        }
      }, []);
    
      React.useEffect(() => {
        const element = ref?.current;
    
        if (!element) return;
    
        const observer = new ResizeObserver((entries) => {
          // 👉 Do something when the element is resized
          const dialogPosition = entries[0].target.getBoundingClientRect();
    
          setX(dialogPosition.x + 10);
          setY(dialogPosition.y - 10);
    
          console.log(entries[0]);
        });
    
        observer.observe(element);
        return () => {
          // Cleanup the observer by unobserving all elements
          observer.disconnect();
        };
      }, [x, y]);
    
      const notify = () => {
        dispatchToast(
          <Toast>
            <ToastTitle>Options configured in Toaster </ToastTitle>
          </Toast>,
          { intent: "info" }
        );
      };
      const customStyles = {
        content: {
          top: "20%",
          left: "20%",
          right: "20%",
          bottom: "20%",
          background: "#e5e5e5"
        }
      };
    
      React.useEffect(() => {
        setIsOpen(true);
      }, []);
    
      return (
        <>
          <Modal
            contentRef={dialogdRef}
            style={customStyles}
            isOpen={isOpen}
            contentLabel="Minimal Modal Example"
          >
            <div>
              <h1> I am a Modal</h1>
              <Button onClick={notify}>Make toast</Button>
            </div>
          </Modal>
    
          <Toaster
            toasterId={toasterId}
            offset={{ horizontal: x, vertical: y }}
            position="top-end"
            pauseOnHover
            pauseOnWindowBlur
            timeout={10000}
          />
        </>
      );
    };
    

    enter image description here