DismissController

Reports a pointer press outside a surface, focus leaving it, or Escape.

Reach for it for a dialog, drawer or popup that closes itself.

Import

import { DismissController, createDismissController } from '@ecopages/radiant-ui/controllers';

Try it

Pressing anywhere outside the panel, or pressing Escape, reports the reason and hides it. The button is a related element: it counts as inside, so pressing it does not dismiss.

Press outside me, or press Escape

Open

<div data-example="dismiss">
	<div data-box>Press outside me, or press Escape</div>
	<button type="button">Reopen</button>
	<p data-status>Open</p>
</div>
import { DismissController } from '@ecopages/radiant-ui/controllers';
 
export function mount(root: HTMLElement): () => void {
	const panel = root.querySelector<HTMLElement>('[data-box]')!;
	const reopen = root.querySelector('button')!;
	const status = root.querySelector('[data-status]')!;
 
	const dismiss = new DismissController({
		getSurface: () => panel,
		getRelated: () => [reopen],
		getOpen: () => !panel.hidden,
		onDismiss: (reason) => {
			panel.hidden = true;
			status.textContent = `Dismissed by ${reason}`;
		},
	});
	dismiss.attach();
 
	reopen.addEventListener('click', () => {
		panel.hidden = false;
		status.textContent = 'Open';
	});
	return () => dismiss.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
getSurfaceThe surface being dismissed.
getRelatedElements outside the surface that still count as inside, such as the trigger.
getOpenNothing is reported while this returns false. Defaults to always open.
onDismissReceives the reason (pointer, focus or escape) and the event.
triggersWhich reasons to listen for. Defaults to all three.
settleFocusJudge a focus-out one microtask later, for surfaces whose contents take focus after the old element blurs.
escapeScopedocument (default) hears Escape from anywhere; tree only when the key event starts inside the surface or a related element.

Lifecycle

attach() adds the listeners on document; destroy() removes them. The surface and related elements are read on every event, so they may change while attached. The controller also has contains(node), isOutside(target) and suppressFocus(), which skips the next focus-out for a press whose focus change is not a dismissal.

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

Protected hooks

Extend the class and override these to change how it decides. A component never extends a controller; a subclass is registered with bindController.

HookDescription
isInside(node)Decides what counts as inside. Override to widen or narrow it, such as a surface that counts a neighbouring toolbar. The default is the surface, the related elements, and anything inside them.