PortalController

Moves a node into document.body, or a container, and carries the origin theme with it.

Reach for it for a surface that must escape clipping or stacking contexts.

Import

import { PortalController, createPortalController } from '@ecopages/radiant-ui/controllers';

Try it

Each press of the button attaches or destroys the controller. While attached the node is a child of body; destroyed, it is back among its siblings.

Portaled node

Parent: div

<div data-example="portal">
	<div data-box>Portaled node</div>
	<button type="button">Toggle portal</button>
	<p data-status>Parent: div</p>
</div>
import { PortalController } from '@ecopages/radiant-ui/controllers';
 
export function mount(root: HTMLElement): () => void {
	const panel = root.querySelector<HTMLElement>('[data-box]')!;
	const toggle = root.querySelector('button')!;
	const status = root.querySelector('[data-status]')!;
 
	const portal = new PortalController({ getNode: () => panel });
 
	toggle.addEventListener('click', () => {
		if (portal.isMounted) {
			portal.destroy();
		} else {
			portal.attach();
		}
		status.textContent = `Parent: ${panel.parentElement!.tagName.toLowerCase()}`;
	});
	return () => portal.destroy();
}

mount runs once for each example element on the page, and the function it returns undoes it; see how the examples run.

Config

OptionDescription
getNodeThe node to move. Resolved on every attach(); null leaves nothing mounted.
getContainerWhere the node goes. Resolved on every attach(); document.body when omitted.

Lifecycle

attach() leaves a comment where the node was and appends the node to the container. destroy() restores the original position among its siblings, skipping the move when something removed the comment in the meantime. A node without a parent is not mounted. Attaching again with the same node, or destroying twice, changes nothing. Attaching again after getNode returns another node puts the old one back and mounts the new one, and a null unmounts. isMounted tells which state it is in. A node that leaves its ancestors loses what they set, so attach() also carries the dark class, the data-theme attribute and the inline --rui-* custom properties of its ancestors onto the node, and destroy() removes them again. The controller remembers where each portaled node came from, so DismissController can treat a node inside a portaled root as inside the surface the root came from.

With a host, createPortalController(host, config) does the same on connect and disconnect; see bindController.

Protected hooks

None. Change behavior through the config callbacks.