pushLayer
elements man ui/behaviors/dismissA dismiss layer: Escape and an outside press close an overlay, with a stack so only the top layer reacts. A layer pushed while another is open sits above it. Escape goes to the top layer only, so closing a menu inside a dialog leaves the dialog open and the next Escape closes the dialog; an outside press goes to the top layer and then down the stack to every layer it is also outside of, so a press outside nested popovers closes them all.
Signaturesfunction pushLayer(el: HTMLElement, options: LayerOptions): Layer; function layerCount(): number; function topLayer(): Layer | undefined; function onScrollbar(e: PointerEvent): boolean; type DismissReason = "escape" | "outside" | "focus-outside";
Options
| Option | Type | Default | What it does |
|---|---|---|---|
onDismiss |
(reason: DismissReason, event: Event) => void |
required | Called when the layer should close. |
escape |
boolean | (() => boolean) |
true |
Escape dismisses. A function is read at the moment of the key press. |
outside |
boolean | ((event: PointerEvent | MouseEvent) => boolean) |
true |
A pointer press outside dismisses. A function gets the event and returns whether it dismisses. |
focusOutside |
boolean | (() => boolean) |
false |
Focus moving outside dismisses, for a non-modal popover. |
onEscapeKeyDown |
(event: KeyboardEvent) => void |
none | Called before an Escape dismiss; event.preventDefault() keeps the layer open. |
onPointerDownOutside |
(event: PointerEvent) => void |
none | Called before an outside dismiss; event.preventDefault() keeps the layer open. |
onFocusOutside |
(event: FocusEvent) => void |
none | Called before a focus-outside dismiss; event.preventDefault() keeps the layer open. |
onInteractOutside |
(event: PointerEvent | MouseEvent | FocusEvent) => void |
none | Called before either outside dismiss, after the specific callback; event.preventDefault() keeps the layer open. |
ancestorScroll |
boolean |
false |
Scrolling an element that contains an inside() element (the trigger) dismisses, with reason "outside" and the scroll event. |
inside |
() => (Element | null | undefined)[] |
none | Elements that count as inside the layer, such as its trigger. |
blockOutside |
boolean |
false |
Blocks pointer interaction with everything below the layer while it is open, for a modal menu. The press that lands outside still dismisses. |
bubbles |
boolean | { escapeKey?: boolean; outsidePress?: boolean } |
{ escapeKey: false, outsidePress: true } |
How a dismiss travels to the layers below. escapeKey: true lets one Escape close a whole tree; outsidePress: false stops an outside press at this layer. A boolean sets both. |
outsidePressEvent |
"pointerdown" | "mousedown" | "click" |
"pointerdown" |
Which event an outside mouse or pen press dismisses on. A touch always waits for the click. |
capture |
boolean | { escapeKey?: boolean; outsidePress?: boolean } |
true |
Listen in the capture phase, so a handler that stops the event still dismisses. false waits for the event to bubble to the document, so stopPropagation() in a handler keeps the layer open. |
"Outside" means the event target is not inside the layer element, not
inside any element inside() returns, and not inside a layer stacked above:
a nested popover rendered elsewhere still counts as inside its parent while
it is open. Mouse and pen dismiss on pointerdown; touch dismisses on the
click that follows, so a scroll gesture that starts outside does not close
the layer. An outside press stops below a modal layer (blockOutside, or a
layer inside a modal <dialog>), at a layer whose callback prevented the
dismiss, and at a layer the press is inside: a press inside a parent popover
closes only the child. Focus moving outside closes every focusOutside
layer it left, top first.
An Escape that belongs to an IME composition never dismisses. A press on an
element injected at the top of the page after the layer opened (a browser
extension's popup) is not outside, and neither is a press on the scrollbar
of the element pressed. onScrollbar(e) is that last test, exported for a
template's own pointer handling.
The layer
| Member | What it does |
|---|---|
el, options |
What the layer was pushed with. |
isTop() |
True while this is the topmost layer. |
release() |
Removes the layer from the stack. Safe to call twice. |
In a template of your own
app/shared/templates/popup/index.ehtmlimport { pushLayer, type Layer } from "@elements/ui/behaviors"; interface PopupState { trigger?: HTMLElement; layer?: Layer; } <Popup (open: boolean = false, private ui: PopupState = {})> <button type="button" aria-expanded={open ? "true" : "false"} oninsert={(e) => ui.trigger = e.target as HTMLElement} onclick={() => open = !open}><slot name="trigger"/></button> <div e:if={open} class="popup" oninsert={(e) => ui.layer = pushLayer(e.target as HTMLElement, { onDismiss: () => open = false, inside: () => [ui.trigger], })} onremove={() => ui.layer?.release()}> <slot/> </div> </Popup>
The trigger is inside, so clicking it toggles the popup instead of
dismissing and reopening it. Pair the layer with float (opens in a new tab) to place
the popup and with focusScope (opens in a new tab) when it takes focus.
Writes
data-layer="<depth>" on the element. With blockOutside,
pointer-events: none on <body> and auto on every open layer, put back
when the last blocking layer releases.
Used by
Chart (opens in a new tab), Combobox (opens in a new tab),
Dialog (opens in a new tab), DropdownMenu (opens in a new tab),
ContextMenu (opens in a new tab), Menubar (opens in a new tab),
NavigationMenu (opens in a new tab), Popover (opens in a new tab),
Select (opens in a new tab). onScrollbar: Kanban (opens in a new tab),
RichText (opens in a new tab), TagsInput (opens in a new tab).