Programmatic control of modals
The PlumaModal component is used to display modal dialogs.
Using the PlumaModal component directly, however, can be difficult to manage,
as it requires manually keeping track of the modal's open state. This also makes
it difficult to reuse the same modal in multiple places, as it involves extra
boilerplate code.
Pluma's Modal Manager is a utility that simplifies the creation and management of modals.
To use a modal via the manager, we first need to define a modal component, which will later be referenced in the manager.
Pluma exports utility types that help define the component's props - it's highly recommended to use these types to ensure type safety and type hints.
PlumaManagedModalProps<Props, ConfirmArg, CloseArg> is a generic type that wraps up the props that are passed
into a modal component invoked by the modal manager. It takes three generic parameters:
Props: The custom props you plan to pass into the modal componentConfirmArg: The argument you will pass into the onConfirm callbackCloseArg: The argument you will pass into the onClose callbackTo define a modal component, you need to use this type as your components' props type.
When the modal is opened, the manager will pass any of your custom Props into
the modal component under the data key. Additionally, two callbacks are available:
onClose: A callback to call when you want to close the modal when the user cancels an actiononConfirm: A callback for when the user confirms an action, or otherwise performs a CTA-like action
Finally, a modalState object is also present. It is required to forward this prop
into the PlumaModal's state prop - it holds the modal's open state and other modal-related props.
Now that we have defined a managed modal component, we can use it with the manager.
The useModals hook returns the modal manager, which allows you to open and interact with modals.
To open a modal, you can use the open method. As the first argument it accepts
the modal component you previously defined. As the second argument, it expects
the additional props that you expect to see under data.
There is also a useModal hook, which accepts a modal component as its argument
and returns a modal manager API bound to that specific component. It's a shortcut
to make it a little easier to reuse the same modal in different places.
The manager's open method returns a unique identifier for the opened modal.
This identifier can be used in the manager's waitForClose method, which returns
a promise that resolves when the modal is closed.
The value returned from this resolved promise is an object with the following properties:
reason: The reason the modal was closed, a string with one of the following values:
close - called when the modal is closed with the onClose callback,
or when the modal is closed by clicking the backdrop, the X close button, or by pressing the escape key
(if those are enabled)confirm - called when the modal is closed with the onConfirm callbackdata: The data that was passed to the modal's onClose or onConfirm callbackexplanation: The explanation for the modal closure. Only present when the modal is closed
through the modal's "native" close mechanisms, such as clicking the backdrop. The explanation
is a string with one of Floating UI's onOpenChange reasons,
close-button-press if closed by clicking the modal's X close button, or
modal-manager:close-all if closed by calling the manager's closeAll methodThis value can be used to handle the modal closure in the parent component.
Sometimes it might be necessary to update a modal's data props after it has been opened.
This can be done with the manager's update method. It takes the modal's identifier
as the first argument, and the new props as the second argument.
The new props will be merged with the existing props, and the modal will be updated accordingly.
If you need to customize any of the Modal component's behavior, you can still use
any of it's arguments as usual. For example, if you'd like make the modal non-dismissable,
you can still use the isDismissable prop to achieve this.
If the configuration needs to depend on arguments you pass into the modal, you may
use your data props to pass that configuration further into the modal.
Modals opened via the modal manager will stay open until they are explicitly closed, either by user action or programmatically.
However, in some cases, you may want to automatically close a modal when the component that opened it is unmounted. For example, when a route transition happens.
Managed modals support this functionality, which can be enabled by passing an additional
configuration argument into the open method: { shouldCloseOnUnmount: true }.
For useModals, this will be the third argument, while for useModal this will be the second argument.
It may sometimes be necessary to render "nested" modals: for example, opening
a confirmation modal from an edit/form modal. In those cases, we always want the
top-most (the most recently opened) modal to be the only one that reacts to
global dismissals like clicking outside or pressing the ESC key.
Linking modals to prevent them from closing when another one is open on top is built into the modal components, and works automatically in many cases. In some cases, however - depending on your component structure - it may be necessary to link the modals explicitly.
The plain Modal component (not the useModals hook) includes a modal context
provider. When modals are nested in one another, the nested one can read the
parent context, and the linking happens automatically.
However, when a useModals hook is called just outside of a Modal, it won't
be able to read that modal's context. useModals needs to be called inside a
component that's nested within Modal for this to work.
Let's look at some examples.
Here, we move the modal's contents into a separate component, so that its
useModals hook can automatically read the parent modal's context.
When the nested modal is opened, clicking inside of it won't close its parent
modal (as it otherwise might because of it being an "outside click" in regards
to the parent), and clicking outside or pressing ESC will only close the nested
modal.
Compare the above to a nested modal that cannot read the parent modal's context:
The Modal component yields its context into its context,
making it possible to manually pass it into useModals.
The open method accepts an optional third parameter, which is an object
(the second parameter is your custom props that are passed into the modal component).
One of the keys it accepts is modalContext, which should be the parent context
we want the managed modal to recognize.