/** * Constants for every `reason` string carried on Base UI change-event details. * Mirrors the React Base UI `REASONS` namespace. */ export const REASONS = { none: "none", triggerPress: "trigger-press", triggerHover: "trigger-hover", triggerFocus: "trigger-focus", outsidePress: "outside-press", itemPress: "item-press", closePress: "close-press", linkPress: "link-press", clearPress: "clear-press", chipRemovePress: "chip-remove-press", trackPress: "track-press", incrementPress: "increment-press", decrementPress: "decrement-press", inputChange: "input-change", inputClear: "input-clear", inputBlur: "input-blur", inputPaste: "input-paste", inputPress: "input-press", focusOut: "focus-out", escapeKey: "escape-key", closeWatcher: "close-watcher", listNavigation: "list-navigation", keyboard: "keyboard", pointer: "pointer", drag: "drag", swipe: "swipe", wheel: "wheel", scrub: "scrub", cancelOpen: "cancel-open", siblingOpen: "sibling-open", disabled: "disabled", imperativeAction: "imperative-action", windowResize: "window-resize", } as const; export type BaseUIEventReasons = typeof REASONS; export type BaseUIEventReason = BaseUIEventReasons[keyof BaseUIEventReasons]; interface ReasonToEventMap { [REASONS.none]: Event; [REASONS.triggerPress]: | MouseEvent | PointerEvent | TouchEvent | KeyboardEvent; [REASONS.triggerHover]: MouseEvent; [REASONS.triggerFocus]: FocusEvent; [REASONS.outsidePress]: MouseEvent | PointerEvent | TouchEvent; [REASONS.itemPress]: MouseEvent | KeyboardEvent | PointerEvent; [REASONS.closePress]: MouseEvent | KeyboardEvent | PointerEvent; [REASONS.linkPress]: MouseEvent | PointerEvent; [REASONS.clearPress]: PointerEvent | MouseEvent | KeyboardEvent; [REASONS.chipRemovePress]: PointerEvent | MouseEvent | KeyboardEvent; [REASONS.trackPress]: PointerEvent | MouseEvent | TouchEvent; [REASONS.incrementPress]: PointerEvent | MouseEvent | TouchEvent; [REASONS.decrementPress]: PointerEvent | MouseEvent | TouchEvent; [REASONS.inputChange]: InputEvent | Event; [REASONS.inputClear]: InputEvent | FocusEvent | Event; [REASONS.inputBlur]: FocusEvent; [REASONS.inputPaste]: ClipboardEvent; [REASONS.inputPress]: MouseEvent | PointerEvent | TouchEvent | KeyboardEvent; [REASONS.focusOut]: FocusEvent | KeyboardEvent; [REASONS.escapeKey]: KeyboardEvent; [REASONS.closeWatcher]: Event; [REASONS.listNavigation]: KeyboardEvent; [REASONS.keyboard]: KeyboardEvent; [REASONS.pointer]: PointerEvent; [REASONS.drag]: PointerEvent | TouchEvent; [REASONS.swipe]: PointerEvent | TouchEvent; [REASONS.wheel]: WheelEvent; [REASONS.scrub]: PointerEvent; [REASONS.cancelOpen]: MouseEvent; [REASONS.siblingOpen]: Event; [REASONS.disabled]: Event; [REASONS.imperativeAction]: Event; [REASONS.windowResize]: UIEvent; } /** * Maps a change `reason` string to its corresponding native event type. */ export type ReasonToEvent = Reason extends keyof ReasonToEventMap ? ReasonToEventMap[Reason] : Event; type BaseUIChangeEventDetail = { /** Reason the event fired. */ reason: Reason; /** Underlying native event. */ event: ReasonToEvent; /** Prevent Base UI's internal handling of the event. */ cancel: () => void; /** * Allow event propagation in cases where Base UI would otherwise stop it. */ allowPropagation: () => void; /** `true` once `cancel()` has been called. */ readonly isCanceled: boolean; /** `true` once `allowPropagation()` has been called. */ readonly isPropagationAllowed: boolean; /** The element that triggered the event, if any. */ trigger: Element | undefined; } & Custom; /** * The "details" object passed as the second argument of every `onXChange` * callback in Base UI. */ export type BaseUIChangeEventDetails< Reason extends string, Custom extends object = object, > = Reason extends string ? BaseUIChangeEventDetail : never; type BaseUIGenericEventDetail = { reason: Reason; event: ReasonToEvent; } & Custom; export type BaseUIGenericEventDetails< Reason extends string, Custom extends object = object, > = Reason extends string ? BaseUIGenericEventDetail : never; const EMPTY_CUSTOM: object = Object.freeze({}); /** * Create a change-event details object with `cancel`/`allowPropagation` * utilities and their read-only observers. * * Plain value factory — not reactive, safe to call in event handlers. */ export function createChangeEventDetails< Reason extends string, Custom extends object = object, >( reason: Reason, event?: ReasonToEvent, trigger?: HTMLElement, customProperties?: Custom, ): BaseUIChangeEventDetails { let canceled = false; let allowPropagation = false; const custom = customProperties ?? (EMPTY_CUSTOM as Custom); const details: BaseUIChangeEventDetail = { reason, event: (event ?? new Event("base-ui")) as ReasonToEvent, cancel() { canceled = true; }, allowPropagation() { allowPropagation = true; }, get isCanceled() { return canceled; }, get isPropagationAllowed() { return allowPropagation; }, trigger, ...custom, }; return details as BaseUIChangeEventDetails; } /** * Create a "generic" (non-cancelable) event details object. Useful for * notification-style events where the consumer cannot influence the flow. */ export function createGenericEventDetails< Reason extends keyof ReasonToEventMap, Custom extends object = object, >( reason: Reason, event?: ReasonToEvent, customProperties?: Custom, ): BaseUIGenericEventDetails { const custom = customProperties ?? (EMPTY_CUSTOM as Custom); const details: BaseUIGenericEventDetail = { reason, event: (event ?? new Event("base-ui")) as ReasonToEvent, ...custom, }; return details as BaseUIGenericEventDetails; }