Popover
<oge-popover> is an anchored, interactive panel — a title, rich body, footer actions, a close button and an optional callout arrow — opened from any element carrying [ogePopover] by click, hover, focus or code. It is a non-modal or modal role="dialog" rendered into the document body.
Basics
Declare an <oge-popover> with a template reference and point [ogePopover] at it from any element. The trigger becomes an APG disclosure — aria-haspopup="dialog", aria-expanded and aria-controls — and the panel is a role="dialog" labelled by its title. *ogePopoverFooter receives a close function for the action bar; Escape, an outside click and the ✕ close it too.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeButton } from '@oge-ui/buttons';
import { OgePopover, OgePopoverTrigger, OgePopoverFooter } from '@oge-ui/overlay';
@Component({
selector: 'demo-root',
imports: [OgeButton, OgePopover, OgePopoverTrigger, OgePopoverFooter],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-button text="Share" [ogePopover]="share" />
<oge-popover #share title="Share report" [arrow]="true">
<p>Anyone with the link can view this report.</p>
<div *ogePopoverFooter="let close">
<oge-button text="Done" stylingMode="text" (clicked)="close()" />
<oge-button text="Copy link" (clicked)="copied(); close()" />
</div>
</oge-popover>
<!-- the trigger gets aria-haspopup="dialog", aria-expanded and
aria-controls; Escape, outside clicks and the ✕ close it -->
`,
})
export class Demo {
protected copied(): void {
console.log('link copied');
}
}Triggers
showOn picks what opens it: click (the default disclosure), hover — a short dwell, then a grace period that lets the pointer travel into the panel, and keyboard focus opens it too so the content is never pointer-only — focus, or manual for popovers driven only from code (#ref="ogePopover" → open() / close() / toggle()).
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeButton } from '@oge-ui/buttons';
import { OgePopover, OgePopoverTrigger } from '@oge-ui/overlay';
@Component({
selector: 'demo-root',
imports: [OgeButton, OgePopover, OgePopoverTrigger],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- click (default): the APG disclosure -->
<oge-button text="Click" [ogePopover]="click" />
<oge-popover #click title="Click">Toggles on activation.</oge-popover>
<!-- hover: dwell + a grace period that survives moving into the panel;
keyboard focus opens it too -->
<oge-button text="Hover" [ogePopover]="hover" />
<oge-popover #hover showOn="hover" [showCloseButton]="false" ariaLabel="Hover help">
Move into me — I stay open. <a href="#hover">Links work</a>.
</oge-popover>
<!-- focus: opens while the trigger (or the panel) has focus -->
<input aria-label="Coupon" placeholder="Coupon code" [ogePopover]="focus" />
<oge-popover #focus showOn="focus" [showCloseButton]="false" ariaLabel="Coupon help">
Codes are case-insensitive.
</oge-popover>
<!-- manual: only code opens it (exportAs: 'ogePopover') -->
<oge-button text="Manual" [ogePopover]="manual" (clicked)="manual.toggle()" />
<oge-popover #manual showOn="manual" title="Manual">Opened from code.</oge-popover>
`,
})
export class Demo {}Placement & arrow
placement prefers a side and alignment (top, right-start, bottom-end…) and flips when the viewport runs out of room, RTL-aware. [arrow]="true" draws a callout pointer on the edge facing the trigger; its geometry is shared with the tooltip and keeps pointing at the trigger after the viewport clamp shifted the panel. width / maxWidth size the content box.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeButton } from '@oge-ui/buttons';
import { OgePopover, OgePopoverTrigger } from '@oge-ui/overlay';
@Component({
selector: 'demo-root',
imports: [OgeButton, OgePopover, OgePopoverTrigger],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-button text="Top" [ogePopover]="top" />
<oge-popover #top placement="top" [arrow]="true" title="Top">
Centered above; flips below when there is no room.
</oge-popover>
<oge-button text="Right start" [ogePopover]="right" />
<oge-popover #right placement="right-start" [arrow]="true" [width]="220" title="Right start">
The arrow keeps pointing at the trigger after the viewport clamp.
</oge-popover>
`,
})
export class Demo {}Modal vs non-modal
Non-modal (the default) follows the APG disclosure: focus stays on the trigger, Tab moves into the panel and on past it — the panel is rendered into the body, yet focus order matches the visual order — and leaving it closes it. [modal]="true" makes it an aria-modal dialog: focus moves inside (initialFocus), Tab is trapped, and focus returns to the trigger on close.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeButton } from '@oge-ui/buttons';
import { OgePopover, OgePopoverTrigger, OgePopoverFooter } from '@oge-ui/overlay';
@Component({
selector: 'demo-root',
imports: [OgeButton, OgePopover, OgePopoverTrigger, OgePopoverFooter],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- non-modal (default): Tab moves from the trigger into the panel and
on past it — focus order matches the visual order -->
<oge-button text="Filter" [ogePopover]="filter" />
<oge-popover #filter title="Filter">
<label>Contains <input /></label>
</oge-popover>
<!-- modal: aria-modal, focus moves in, Tab is trapped, focus returns -->
<oge-button text="Rename" [ogePopover]="rename" />
<oge-popover #rename title="Rename file" [modal]="true">
<label>Name <input value="report.xlsx" /></label>
<div *ogePopoverFooter="let close">
<oge-button text="Cancel" stylingMode="text" (clicked)="close()" />
<oge-button text="Save" (clicked)="close()" />
</div>
</oge-popover>
`,
})
export class Demo {}Events & imperative API
[(visible)] is the two-way open state; writes from code run the same pipeline. opening and closing are cancelable and carry the reason (click, hover, focus, api / trigger, pointerLeave, focusOut, outside, escape, closeButton); opened and closed follow. Here “Pin” vetoes every close except the ✕.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeButton } from '@oge-ui/buttons';
import { OgePopover, OgePopoverTrigger } from '@oge-ui/overlay';
import type { OgePopoverClosingEvent } from '@oge-ui/overlay';
@Component({
selector: 'demo-root',
imports: [OgeButton, OgePopover, OgePopoverTrigger],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-button text="Details" [ogePopover]="details" />
<!-- opened from code, it anchors to its first [ogePopover] trigger
(or to [anchor]) -->
<oge-button text="Open from code" stylingMode="outlined" (clicked)="details.open()" />
<oge-popover
#details
title="Order #1042"
[(visible)]="visible"
(opened)="log('opened: ' + $event.reason)"
(closing)="guard($event)"
(closed)="log('closed: ' + $event.reason)"
>
Shipped today.
</oge-popover>
`,
})
export class Demo {
protected readonly visible = signal(false);
protected readonly pinned = signal(false);
// closing is cancelable: keep the popover while "pinned"
protected guard(event: OgePopoverClosingEvent): void {
event.cancel = this.pinned() && event.reason !== 'closeButton';
}
protected log(message: string): void {
console.log(message);
}
}Notes
- A popover is interactive; a tooltip is not. Use a tooltip for a short description of its trigger, a popover for content people act on.
- Escape goes through the shared overlay stack: a select box opened inside a popover closes first, then the popover.
- The trigger's focusable control carries the ARIA (the inner
<button>of anoge-button); without a trigger, bind[anchor]for popovers opened from code.