0

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?

user3587624
  • 1,427
  • 5
  • 29
  • 60
  • you can check fluentui toaster demo i use react-modal for the demo and i update the answer and check resolved – Christa Aug 25 '23 at 20:40

2 Answers2

2

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

Christa
  • 398
  • 7
  • It seems that some of the properties you added are not present in my Toast component. I am using this one in case it helps: https://react.fluentui.dev/?path=/docs/components-toast--default – user3587624 Aug 25 '23 at 03:45
  • 1
    @user3587624 i update the anser the fluterui toaster component doest provide a mountnode property to set the toaster container and by default the toaster append all toasts at the body (because its wrapped under portal) and you can use callback and ref to get x,y of the modal and ajust position of the toast like in the seconde example but you need to handle resizing cases by adding observer... – Christa Aug 25 '23 at 10:02
0

Yes, you can achieve this by using the useRef hook. The useRef hook creates a mutable ref that you can use to store a value. The value of the ref can be changed at any time, and the changes will be reflected in the rendered output.

Here is the code with the useRef hook:

const dialogRef = useRef(null);

const getAbsPositionDialog = () => {
  if (!dialogRef.current) {
    return { x: 0, y: 0 };
  }
  const dialogPosition = dialogRef.current.getBoundingClientRect();
  return { x: dialogPosition.x, y: dialogPosition.y };
}

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

...

React.useEffect(() => {
  dialogRef.current = document.getElementById('modalDialogId');
}, []);

...

In this code, the dialogRef ref is created and initialized to null. The getAbsPositionDialog function is then called to get the absolute position of the modal dialog. The horizontal and vertical variables are then set to the x and y coordinates of the modal dialog, respectively.

The useEffect hook is then used to set the dialogRef ref to the document.getElementById('modalDialogId') element. This is done so that the dialogRef ref is always pointing to the modal dialog element.

The useEffect hook also takes an empty array as its second argument. This means that the useEffect hook will only run once, when the component is first rendered.

I hope this helps! Let me know if you have any other questions.