iTwin / iTwinUI-react

A react component library for iTwinUI.
https://github.com/iTwin/iTwinUI
Other
83 stars 23 forks source link

Toast : Allow link props to be a ReactNode. #116

Closed maxime4000 closed 3 years ago

maxime4000 commented 3 years ago

Feature

Toast are good to display small text of information. The link props is mostly use to view the full details of the toast. Where would you display that information in your application ? My first choice is a modal. But how can you generate a modal dynamically from a onClick event ? For toast, the Toaster class can generate Toast with his function, but there is no such option with Modal. So there is a few way to make this happen. I would go with option 3.

  1. Have a Modal equivalent of Toaster (which btw toaster is not the React way to do those thing).
  2. Have a ModalProvider and hook useModal() : { addModal({}: ModalProps) }
  3. Allow custom JSX element in toast Props so I can use a custom button/anchor to spawn a modal from it.

Examples

  1. Modal factory (Modaler)
    toaster.positive(
    <>
        <div>All those operation has work : {success.length}</div>
    </>,
    {
        link: {
            onClick: () => {
                Modaler.addModal({
                    isOpen: true,
                    id: "MoreInfoModal",
                    title: t("keyMoreInfo"),
                    onClose: () => {},
                    isDismissible: true,
                    closeOnEsc: false,
                    closeOnExternalClick: false,
                });
            },
            title: t("keyShowMoreInfo"),
        },
        type: "persisting",
    }
    );
  2. ModalProvider and Hook :
    const useCustomToast = () => {
    const {addModal} = useModal();
    return (obj) => 
    toaster.positive(<></>, {
      link: { 
        onClick: () => { addModal({/*Props*/});}
      }
    );
    };
  3. JSX in Toast Props :
    
    toaster.positive(<></>, { link: <CustomModal info={obj} /> });

const CustomModalButton = ({info}) => { const [ isOpen, toggleOpen] = useToggle(false); return <> <Button onClick={() => toggleOpen()}>View more {isOpen && ( <Modal isOpen={isOpen} title={t("keyMoreInfo")} onClose={onClose} isDismissible={true} closeOnEsc={false} closeOnExternalClick={false}> {info} )} </> }

mayank99 commented 3 years ago

I don't understand the problem here. Can you clarify why something like the following snippet will not work for you?

const [isModalOpen, setIsModalOpen] = React.useState(false);
toaster.informational("Toast message", { link: { title: "Details", onClick: () => setIsModalOpen(true) } });
maxime4000 commented 3 years ago

From where I instantiate the toast, I'm in a service and not in any React component. I don't have a parent. It's simply a function that call the function of toaster that create a toast in ToastMaster. In fact, scratch option 2, it would probably don't work for me as I'm not in a hook.

veekeys commented 3 years ago

Can you try calling ReactDOM.render(<Modal ..../>) from the onClick callback?

maxime4000 commented 3 years ago

I can make the modal appear, but I still need someone to control the isOpen and onClose props. So I can render a wrapper component to do that, but I'm still instantiating a component that will stay active in the site and I cannot unmount it. It's better than nothing, but not quite a good solution. I can still wait for a better solution before doing the work.

The simpliest solution would be that link can be a ReactNode element so when the Toast disappear, the modal is unmount. I could also layout my toast to have my own link in the content, but I prefer if the solution I come with is more official/supported and not an hack to make it work.

mayank99 commented 3 years ago

Supporting ReactNode in link may make it simple on your end, but it sends the "mess" on our side and complicates the API (as it needs to support title/onClick as well as custom ReactNode).

I can render a wrapper component to do that, but I'm still instantiating a component that will stay active in the site and I cannot unmount it.

This is not different from how our toaster currently works. See this line: https://github.com/iTwin/iTwinUI-react/blob/dfa94c8f4ee22be1cb8a7dc478be55040c51ed6e/src/core/Toast/Toaster.tsx#L73

The toast wrapper div and container stays in the DOM even when all toasts are cleared.

If you want, you can even try rendering your modal in the same container by passing 'iui-toasts-container' into getContainer.

Example code ```tsx type CustomToastProps = { message: string; linkText: string; modalTitle: string; modalContent: ReactNode; }; const showCustomToast = ({ message, linkText, modalTitle, modalContent }) => { const container = getContainer('iui-toasts-container'); const showModal = (isOpen) => { ReactDOM.render( isOpen && {modalContent}, container ); }; const hideModal = () => { showModal(false) }; toaster.informational(message, { link: { title: linkText, onClick: () => showModal(true) } }); } ```
mayank99 commented 3 years ago

Closing this since it's fairly trivial to render modal from toast.

See working example: https://codesandbox.io/s/itwinui-react-minimal-example-forked-eyi59?file=/src/CustomToaster.tsx