Overlay
@oge-ui/overlay is the suite's anchored-popup foundation: pure placement math with flip and viewport clamping, a DI-free panel behavior model, minimal popup chrome and a WAI-ARIA menu list. Panels render inline in the owner's template — there is no portal, and the package renders no user-facing strings of its own.
Anchored panel
An OgeAnchoredPanel pairs an anchor element with a panel element and keeps the panel positioned: preferred placement, main-axis flip when the opposite side has more room, viewport clamping, repositioning on scroll/resize and panel growth. <oge-popup> supplies the fixed-position chrome and stays transparent until the first measure. Outside pointer-down and Escape close it by default.
import { ChangeDetectionStrategy, Component, ElementRef, signal, viewChild } from '@angular/core';
import { OgeButton } from '@oge-ui/buttons';
import { OgePopup, OgeAnchoredPanel } from '@oge-ui/overlay';
import type { OgePopupPlacement } from '@oge-ui/overlay';
@Component({
selector: 'demo-root',
imports: [OgeButton, OgePopup],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<span #anchor class="inline-flex">
<oge-button text="Toggle panel" ariaHasPopup="dialog"
[ariaExpanded]="open()" [ariaControls]="panel.panelId"
(clicked)="open.set(!open())" />
</span>
@if (open()) {
<oge-popup [panel]="panel">
<div class="w-56 p-3">Anchored content…</div>
</oge-popup>
}
`,
})
export class Demo {
readonly open = signal(false);
readonly placement = signal<OgePopupPlacement>('bottom-start');
private readonly anchorRef = viewChild.required<ElementRef<HTMLElement>>('anchor');
private readonly popupRef = viewChild(OgePopup, { read: ElementRef });
readonly panel = new OgeAnchoredPanel({
anchor: () => this.anchorRef().nativeElement,
panel: () => this.popupRef()?.nativeElement ?? null,
placement: () => this.placement(),
onClosed: () => this.open.set(false),
});
}Menu list
<oge-menu-list> implements the WAI-ARIA menu pattern with aria-activedescendant: the container holds real focus, arrows wrap and skip disabled items and separators, Home/End jump, printable keys type-ahead, Enter/Space activate. It is presentation-only — closing is delegated to the owner via closeRequest. Here it renders standalone (no popup) so the keyboard behavior is easy to try.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeMenuList } from '@oge-ui/overlay';
import type { OgeMenuItem } from '@oge-ui/overlay';
@Component({
selector: 'demo-root',
imports: [OgeMenuList],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-menu-list
[items]="items"
ariaLabel="Demo actions"
(itemClick)="last.set($event.item.text)"
/>
`,
})
export class Demo {
protected readonly last = signal('');
protected readonly items: OgeMenuItem[] = [
{ text: 'Duplicate', checked: false },
{ text: 'Move to…' },
{ text: '', separator: true },
{ text: 'Delete', severity: 'danger' },
];
}Notes
resolvePopupPositionis a pure function — unit-test your own overlay placement without any DOM.- Render the popup subtree behind
@if (panel.isOpen())and callpanel.destroy()fromDestroyRef; stacked overlays share a stack so Escape only closes the topmost. OgeMenuItemis the canonical menu item of the suite — the same type drives the drop-down button in both render layers.