All files / components popover.ts

100% Statements 5/5
100% Branches 22/22
100% Functions 4/4
100% Lines 5/5

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71                                                                                                14x                             14x   13x 9x 4x      
import type {
  ChangeDetails,
  ControllableValueOptions,
  RuntimeController,
  RuntimeEventSource,
} from '../core/types';
import type { FloatingPositionOptions } from '../positioning/positioning';
import {
  createOpenController,
  type OpenChangeReason,
  type OpenLifecycleEvents,
  type OpenSnapshot,
  type OverlayElements,
} from './openable';
 
/** Popover options for state, dismissal, focus, and collision-aware positioning. @public */
export interface PopoverOptions extends Partial<
  ControllableValueOptions<boolean, OpenChangeReason>
> {
  /** Deterministic popover ID. */
  readonly id?: string;
  /** Closes on outside focus when true. @defaultValue `true` */
  readonly closeOnFocusOutside?: boolean;
  /** Moves initial focus into the popover when true. @defaultValue `false` */
  readonly focusContent?: boolean;
  /** Restores trigger focus after keyboard dismissal. @defaultValue `true` */
  readonly restoreFocus?: boolean;
  /** Shared placement and collision configuration. */
  readonly positioning?: FloatingPositionOptions;
}
 
/** Headless Popover command, event, binding, and positioning surface. @public */
export interface PopoverController
  extends RuntimeController<OpenSnapshot>, RuntimeEventSource<OpenLifecycleEvents> {
  /** Opens with a typed cause. */
  open(details?: ChangeDetails<OpenChangeReason>): void;
  /** Closes with a typed cause. */
  close(details?: ChangeDetails<OpenChangeReason>): void;
  /** Requests the opposite state. */
  toggle(details?: ChangeDetails<OpenChangeReason>): void;
  /** Binds trigger, content, optional branches, and optional virtual anchor. */
  bind(elements: OverlayElements): () => void;
  /** Recalculates coordinates after consumer-rendered size changes. */
  updatePosition(): void;
}
 
/** Creates a collision-aware Popover that treats descendant overlay branches as inside. @public */
export function createPopover(options: PopoverOptions = {}): PopoverController {
  const base = createOpenController({
    role: 'dialog',
    modal: false,
    trapFocus: false,
    focusOnOpen: options.focusContent ?? false,
    restoreFocusOnOutside: false,
    closeOnFocusOutside: options.closeOnFocusOutside ?? true,
    ...(options.id ? { id: options.id } : {}),
    ...(options.defaultValue !== undefined ? { defaultValue: options.defaultValue } : {}),
    ...(options.getValue ? { getValue: options.getValue } : {}),
    ...(options.onValueChange ? { onValueChange: options.onValueChange } : {}),
    ...(options.subscribeValue ? { subscribeValue: options.subscribeValue } : {}),
    ...(options.restoreFocus !== undefined ? { restoreFocus: options.restoreFocus } : {}),
    ...(options.positioning ? { positioning: options.positioning } : {}),
  });
  return {
    ...base,
    open: (changeDetails = { reason: 'programmatic' }) => base.open(changeDetails),
    close: (changeDetails = { reason: 'programmatic' }) => base.close(changeDetails),
    toggle: (changeDetails = { reason: 'programmatic' }) => base.toggle(changeDetails),
  };
}