ng-primitives 0.122.0 → 0.123.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"ng-primitives-utils.mjs","sources":["../../../../packages/ng-primitives/utils/src/forms/providers.ts","../../../../packages/ng-primitives/utils/src/observables/take-until-destroyed.ts","../../../../packages/ng-primitives/utils/src/forms/status.ts","../../../../packages/ng-primitives/utils/src/helpers/attributes.ts","../../../../packages/ng-primitives/utils/src/helpers/disposables.ts","../../../../packages/ng-primitives/utils/src/helpers/unique-id.ts","../../../../packages/ng-primitives/utils/src/helpers/validators.ts","../../../../packages/ng-primitives/utils/src/signals/index.ts","../../../../packages/ng-primitives/utils/src/ng-primitives-utils.ts"],"sourcesContent":["import { ExistingProvider, Type } from '@angular/core';\nimport { NG_VALUE_ACCESSOR } from '@angular/forms';\n\n/**\n * A simple helper function to provide a value accessor for a given type.\n * @param type The type to provide the value accessor for\n */\nexport function provideValueAccessor<T>(type: Type<T>): ExistingProvider {\n return { provide: NG_VALUE_ACCESSOR, useExisting: type, multi: true };\n}\n","/* eslint-disable @nx/workspace-take-until-destroyed */\nimport { DestroyRef } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { EMPTY, MonoTypeOperatorFunction, NEVER, pipe } from 'rxjs';\nimport { catchError, defaultIfEmpty, takeUntil } from 'rxjs/operators';\n\n/**\n * The built-in `takeUntilDestroyed` operator does not handle the case when the component is destroyed before the source observable emits.\n * This operator ensures that the source observable completes gracefully without throwing an error.\n * https://github.com/angular/angular/issues/54527#issuecomment-2098254508\n *\n * @internal\n */\nexport function safeTakeUntilDestroyed<T>(destroyRef?: DestroyRef): MonoTypeOperatorFunction<T> {\n return pipe(\n takeUntil(\n NEVER.pipe(\n takeUntilDestroyed(destroyRef),\n catchError(() => EMPTY),\n defaultIfEmpty(null),\n ),\n ),\n );\n}\n","import {\n DestroyRef,\n Injector,\n Signal,\n WritableSignal,\n effect,\n inject,\n signal,\n untracked,\n} from '@angular/core';\nimport { NgControl } from '@angular/forms';\nimport { onMount } from 'ng-primitives/state';\nimport { safeTakeUntilDestroyed } from '../observables/take-until-destroyed';\n\nexport interface NgpControlStatus {\n valid: boolean | null;\n invalid: boolean | null;\n pristine: boolean | null;\n dirty: boolean | null;\n touched: boolean | null;\n pending: boolean | null;\n disabled: boolean | null;\n}\n\n/**\n * Detects an Angular signal-forms interop control without importing any of the new\n * signal-form types. Interop controls expose a `field()` method which returns the\n * underlying FieldState.\n */\nfunction isInteropControl(control: NgControl | null | undefined): boolean {\n return !!control && typeof (control as any).field === 'function';\n}\n\n/**\n * Reads status from a control and updates the status signal.\n * Wrapped in try-catch to handle signal-forms interop controls where\n * the `field` input may not be available yet (throws NG0950).\n */\nfunction updateStatus(control: NgControl, status: WritableSignal<NgpControlStatus>): void {\n try {\n // For interop controls, read directly from the control (which has signal getters).\n // For classic controls, read from the underlying AbstractControl.\n const source = isInteropControl(control) ? control : ((control as any).control ?? control);\n\n const newStatus: NgpControlStatus = {\n valid: source.valid ?? null,\n invalid: source.invalid ?? null,\n pristine: source.pristine ?? null,\n dirty: source.dirty ?? null,\n touched: source.touched ?? null,\n pending: source.pending ?? null,\n disabled: source.disabled ?? null,\n };\n\n untracked(() => status.set(newStatus));\n } catch {\n // NG0950: Required input not available yet. The effect will re-run\n // when the signal input becomes available.\n }\n}\n\n/**\n * A utility function to get the status of an Angular form control as a reactive signal.\n * This function injects the NgControl and returns a signal that reflects the control's status.\n * It supports both classic reactive forms controls and signal-forms interop controls.\n * @internal\n */\n/**\n * Sets up event subscription for a given NgControl.\n * Only sets up the subscription - does not call updateStatus.\n */\nfunction setupEventSubscription(\n ngControl: NgControl,\n status: WritableSignal<NgpControlStatus>,\n destroyRef: DestroyRef,\n): void {\n // For classic controls, also subscribe to the events observable.\n const underlyingControl = (ngControl as any).control;\n if (underlyingControl?.events) {\n underlyingControl.events\n .pipe(safeTakeUntilDestroyed(destroyRef))\n .subscribe(() => updateStatus(ngControl, status));\n }\n}\n\nexport function controlStatus(): Signal<NgpControlStatus> {\n const injector = inject(Injector);\n const destroyRef = inject(DestroyRef);\n\n const status = signal<NgpControlStatus>({\n valid: null,\n invalid: null,\n pristine: null,\n dirty: null,\n touched: null,\n pending: null,\n disabled: null,\n });\n\n const control = signal<NgControl | null>(null);\n\n onMount(() => {\n // Try to inject NgControl immediately for initial state\n control.set(inject(NgControl, { optional: true }));\n\n // If we have a control immediately, update initial status\n if (control()) {\n updateStatus(control()!, status);\n }\n\n // Get the control (either from initial injection or from mount)\n const mountControl = control() || inject(NgControl, { optional: true });\n\n if (!mountControl) {\n return;\n }\n\n // Update control signal if it wasn't set before\n if (!control()) {\n control.set(mountControl);\n }\n\n // Update status to ensure latest values\n updateStatus(mountControl, status);\n\n // Set up event subscription for reactive updates\n setupEventSubscription(mountControl, status, destroyRef);\n });\n\n // Use an effect to reactively track status changes.\n // For signal-forms interop controls, the status properties are signals.\n // For classic controls, this will read the current values and establish\n // no signal dependencies, but we also subscribe to events below.\n effect(\n () => {\n const c = control();\n if (c) {\n updateStatus(c, status);\n }\n },\n { injector },\n );\n\n return status;\n}\n","import { afterRenderEffect, Signal } from '@angular/core';\n\nexport function booleanAttributeBinding(\n element: HTMLElement,\n attribute: string,\n value: Signal<boolean> | undefined,\n): void {\n // eslint-disable-next-line @angular-eslint/no-uncalled-signals -- checking whether the optional signal was provided, not its value\n if (value === undefined) {\n return;\n }\n\n afterRenderEffect({\n write: () =>\n value() ? element.setAttribute(attribute, '') : element.removeAttribute(attribute),\n });\n}\n","import { DestroyRef, inject } from '@angular/core';\n\n/**\n * Disposable functions are a way to manage timers, intervals, and event listeners\n * that should be cleared when a component is destroyed.\n *\n * This is heavily inspired by Headless UI disposables:\n * https://github.com/tailwindlabs/headlessui/blob/main/packages/%40headlessui-react/src/utils/disposables.ts\n */\nexport function injectDisposables() {\n const destroyRef = inject(DestroyRef);\n let isDestroyed = false;\n\n destroyRef.onDestroy(() => (isDestroyed = true));\n\n return {\n /**\n * Set a timeout that will be cleared when the component is destroyed.\n * @param callback The callback to execute\n * @param delay The delay before the callback is executed\n * @returns A function to clear the timeout\n */\n setTimeout: (callback: () => void, delay: number) => {\n if (isDestroyed) {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {};\n }\n\n const id = setTimeout(callback, delay);\n const cleanup = () => clearTimeout(id);\n destroyRef.onDestroy(cleanup);\n return cleanup;\n },\n /**\n * Set an interval that will be cleared when the component is destroyed.\n * @param callback The callback to execute\n * @param delay The delay before the callback is executed\n * @param target\n * @param type\n * @param listener\n * @param options\n * @returns A function to clear the interval\n */\n addEventListener: <K extends keyof HTMLElementEventMap>(\n target: EventTarget,\n type: K,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any,\n options?: boolean | AddEventListenerOptions,\n ) => {\n if (isDestroyed) {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {};\n }\n\n target.addEventListener(type, listener as EventListenerOrEventListenerObject, options);\n const cleanup = () =>\n target.removeEventListener(type, listener as EventListenerOrEventListenerObject, options);\n destroyRef.onDestroy(cleanup);\n return cleanup;\n },\n /**\n * Set an interval that will be cleared when the component is destroyed.\n * @param callback The callback to execute\n * @param delay The delay before the callback is executed\n * @returns A function to clear the interval\n */\n setInterval: (callback: () => void, delay: number) => {\n if (isDestroyed) {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {};\n }\n\n const id = setInterval(callback, delay);\n const cleanup = () => clearInterval(id);\n destroyRef.onDestroy(cleanup);\n return cleanup;\n },\n /**\n * Set a requestAnimationFrame that will be cleared when the component is destroyed.\n * @param callback The callback to execute\n * @returns A function to clear the requestAnimationFrame\n */\n requestAnimationFrame: (callback: FrameRequestCallback) => {\n if (isDestroyed) {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {};\n }\n\n const id = requestAnimationFrame(callback);\n const cleanup = () => cancelAnimationFrame(id);\n destroyRef.onDestroy(cleanup);\n return cleanup;\n },\n };\n}\n","/**\n * Store a map of unique ids for elements so that there are no collisions.\n */\nconst uniqueIdMap = new Map<string, number>();\n\n/**\n * Generate a unique id for an element\n * @param prefix - The prefix to use for the id\n * @returns The generated id\n */\nexport function uniqueId(prefix: string): string {\n const id = uniqueIdMap.get(prefix) ?? 0;\n uniqueIdMap.set(prefix, id + 1);\n return `${prefix}-${id}`;\n}\n","/**\n * Type validation utilities\n */\n\n/**\n * Checks if a value is a string\n * @param value - The value to check\n * @returns true if the value is a string, false otherwise\n */\nexport function isString(value: unknown): value is string {\n return typeof value === 'string';\n}\n\n/**\n * Checks if a value is a number\n * @param value - The value to check\n * @returns true if the value is a number, false otherwise\n */\nexport function isNumber(value: unknown): value is number {\n return typeof value === 'number';\n}\n\n/**\n * Checks if a value is a boolean\n * @param value - The value to check\n * @returns true if the value is a boolean, false otherwise\n */\nexport function isBoolean(value: unknown): value is boolean {\n return typeof value === 'boolean';\n}\n\n/**\n * Checks if a value is a function\n * @param value - The value to check\n * @returns true if the value is a function, false otherwise\n */\nexport function isFunction(value: unknown): value is CallableFunction {\n return typeof value === 'function';\n}\n\n/**\n * Checks if a value is a plain object (but not null or array)\n * @param value - The value to check\n * @returns true if the value is a plain object, false otherwise\n */\nexport function isObject(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === 'object' && !Array.isArray(value);\n}\n\n/**\n * Checks if a value is undefined\n * @param value - The value to check\n * @returns true if the value is undefined, false otherwise\n */\nexport function isUndefined(value: unknown): value is undefined {\n return typeof value === 'undefined';\n}\n\n/**\n * Checks if a value is null or undefined\n * @param value - The value to check\n * @returns true if the value is null or undefined, false otherwise\n */\nexport function isNil(value: unknown): value is null | undefined {\n return isUndefined(value) || value === null;\n}\n\n/**\n * Checks if a value is not null and not undefined\n * @param value - The value to check\n * @returns true if the value is not null and not undefined, false otherwise\n */\nexport function notNil<T>(value: T | null | undefined): value is T {\n return !isNil(value);\n}\n","import { effect, Injector, Signal, signal, untracked } from '@angular/core';\n\n/**\n * Listen for changes to a signal and call a function when the signal changes.\n * @param source\n * @param fn\n * @param options\n * @param options.injector\n * @internal\n */\nexport function onChange<T>(\n source: Signal<T>,\n fn: (value: T, previousValue: T | null | undefined) => void,\n options?: { injector: Injector },\n): void {\n const previousValue = signal(source());\n\n effect(\n () => {\n const value = source();\n if (value !== previousValue()) {\n untracked(() => fn(value, previousValue()));\n previousValue.set(value);\n }\n },\n { injector: options?.injector },\n );\n\n // call the fn with the initial value\n fn(source(), null);\n}\n\n/**\n * Listen for changes to a boolean signal and call one of two functions when the signal changes.\n * @param source\n * @param onTrue\n * @param onFalse\n * @param options\n */\nexport function onBooleanChange(\n source: Signal<boolean>,\n onTrue?: () => void,\n onFalse?: () => void,\n options?: { injector: Injector },\n): void {\n onChange(source, value => (value ? onTrue?.() : onFalse?.()), options);\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;;;AAGA;;;AAGG;AACG,SAAU,oBAAoB,CAAI,IAAa,EAAA;AACnD,IAAA,OAAO,EAAE,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;AACvE;;ACHA;;;;;;AAMG;AACG,SAAU,sBAAsB,CAAI,UAAuB,EAAA;AAC/D,IAAA,OAAO,IAAI,CACT,SAAS,CACP,KAAK,CAAC,IAAI,CACR,kBAAkB,CAAC,UAAU,CAAC,EAC9B,UAAU,CAAC,MAAM,KAAK,CAAC,EACvB,cAAc,CAAC,IAAI,CAAC,CACrB,CACF,CACF;AACH;;ACCA;;;;AAIG;AACH,SAAS,gBAAgB,CAAC,OAAqC,EAAA;IAC7D,OAAO,CAAC,CAAC,OAAO,IAAI,OAAQ,OAAe,CAAC,KAAK,KAAK,UAAU;AAClE;AAEA;;;;AAIG;AACH,SAAS,YAAY,CAAC,OAAkB,EAAE,MAAwC,EAAA;AAChF,IAAA,IAAI;;;QAGF,MAAM,MAAM,GAAG,gBAAgB,CAAC,OAAO,CAAC,GAAG,OAAO,IAAK,OAAe,CAAC,OAAO,IAAI,OAAO,CAAC;AAE1F,QAAA,MAAM,SAAS,GAAqB;AAClC,YAAA,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,IAAI;AAC3B,YAAA,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,IAAI;AAC/B,YAAA,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,IAAI;AACjC,YAAA,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,IAAI;AAC3B,YAAA,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,IAAI;AAC/B,YAAA,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,IAAI;AAC/B,YAAA,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,IAAI;SAClC;QAED,SAAS,CAAC,MAAM,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACxC;AAAE,IAAA,MAAM;;;IAGR;AACF;AAEA;;;;;AAKG;AACH;;;AAGG;AACH,SAAS,sBAAsB,CAC7B,SAAoB,EACpB,MAAwC,EACxC,UAAsB,EAAA;;AAGtB,IAAA,MAAM,iBAAiB,GAAI,SAAiB,CAAC,OAAO;AACpD,IAAA,IAAI,iBAAiB,EAAE,MAAM,EAAE;AAC7B,QAAA,iBAAiB,CAAC;AACf,aAAA,IAAI,CAAC,sBAAsB,CAAC,UAAU,CAAC;aACvC,SAAS,CAAC,MAAM,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACrD;AACF;SAEgB,aAAa,GAAA;AAC3B,IAAA,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;AACjC,IAAA,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;IAErC,MAAM,MAAM,GAAG,MAAM,CAAmB;AACtC,QAAA,KAAK,EAAE,IAAI;AACX,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,QAAQ,EAAE,IAAI;AACd,QAAA,KAAK,EAAE,IAAI;AACX,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,QAAQ,EAAE,IAAI;AACf,KAAA,EAAA,IAAA,SAAA,GAAA,CAAA,EAAA,SAAA,EAAA,QAAA,EAAA,CAAA,8BAAA,EAAA,CAAA,CAAC;AAEF,IAAA,MAAM,OAAO,GAAG,MAAM,CAAmB,IAAI,8EAAC;IAE9C,OAAO,CAAC,MAAK;;AAEX,QAAA,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;;QAGlD,IAAI,OAAO,EAAE,EAAE;AACb,YAAA,YAAY,CAAC,OAAO,EAAG,EAAE,MAAM,CAAC;QAClC;;AAGA,QAAA,MAAM,YAAY,GAAG,OAAO,EAAE,IAAI,MAAM,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAEvE,IAAI,CAAC,YAAY,EAAE;YACjB;QACF;;AAGA,QAAA,IAAI,CAAC,OAAO,EAAE,EAAE;AACd,YAAA,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;QAC3B;;AAGA,QAAA,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC;;AAGlC,QAAA,sBAAsB,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,CAAC;AAC1D,IAAA,CAAC,CAAC;;;;;IAMF,MAAM,CACJ,MAAK;AACH,QAAA,MAAM,CAAC,GAAG,OAAO,EAAE;QACnB,IAAI,CAAC,EAAE;AACL,YAAA,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC;QACzB;AACF,IAAA,CAAC,EACD,EAAE,QAAQ,EAAE,CACb;AAED,IAAA,OAAO,MAAM;AACf;;SC9IgB,uBAAuB,CACrC,OAAoB,EACpB,SAAiB,EACjB,KAAkC,EAAA;;AAGlC,IAAA,IAAI,KAAK,KAAK,SAAS,EAAE;QACvB;IACF;AAEA,IAAA,iBAAiB,CAAC;QAChB,KAAK,EAAE,MACL,KAAK,EAAE,GAAG,OAAO,CAAC,YAAY,CAAC,SAAS,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,eAAe,CAAC,SAAS,CAAC;AACrF,KAAA,CAAC;AACJ;;ACdA;;;;;;AAMG;SACa,iBAAiB,GAAA;AAC/B,IAAA,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;IACrC,IAAI,WAAW,GAAG,KAAK;AAEvB,IAAA,UAAU,CAAC,SAAS,CAAC,OAAO,WAAW,GAAG,IAAI,CAAC,CAAC;IAEhD,OAAO;AACL;;;;;AAKG;AACH,QAAA,UAAU,EAAE,CAAC,QAAoB,EAAE,KAAa,KAAI;YAClD,IAAI,WAAW,EAAE;;AAEf,gBAAA,OAAO,MAAK,EAAE,CAAC;YACjB;YAEA,MAAM,EAAE,GAAG,UAAU,CAAC,QAAQ,EAAE,KAAK,CAAC;YACtC,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,EAAE,CAAC;AACtC,YAAA,UAAU,CAAC,SAAS,CAAC,OAAO,CAAC;AAC7B,YAAA,OAAO,OAAO;QAChB,CAAC;AACD;;;;;;;;;AASG;AACH,QAAA,gBAAgB,EAAE,CAChB,MAAmB,EACnB,IAAO;;QAEP,QAAgE,EAChE,OAA2C,KACzC;YACF,IAAI,WAAW,EAAE;;AAEf,gBAAA,OAAO,MAAK,EAAE,CAAC;YACjB;YAEA,MAAM,CAAC,gBAAgB,CAAC,IAAI,EAAE,QAA8C,EAAE,OAAO,CAAC;AACtF,YAAA,MAAM,OAAO,GAAG,MACd,MAAM,CAAC,mBAAmB,CAAC,IAAI,EAAE,QAA8C,EAAE,OAAO,CAAC;AAC3F,YAAA,UAAU,CAAC,SAAS,CAAC,OAAO,CAAC;AAC7B,YAAA,OAAO,OAAO;QAChB,CAAC;AACD;;;;;AAKG;AACH,QAAA,WAAW,EAAE,CAAC,QAAoB,EAAE,KAAa,KAAI;YACnD,IAAI,WAAW,EAAE;;AAEf,gBAAA,OAAO,MAAK,EAAE,CAAC;YACjB;YAEA,MAAM,EAAE,GAAG,WAAW,CAAC,QAAQ,EAAE,KAAK,CAAC;YACvC,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,EAAE,CAAC;AACvC,YAAA,UAAU,CAAC,SAAS,CAAC,OAAO,CAAC;AAC7B,YAAA,OAAO,OAAO;QAChB,CAAC;AACD;;;;AAIG;AACH,QAAA,qBAAqB,EAAE,CAAC,QAA8B,KAAI;YACxD,IAAI,WAAW,EAAE;;AAEf,gBAAA,OAAO,MAAK,EAAE,CAAC;YACjB;AAEA,YAAA,MAAM,EAAE,GAAG,qBAAqB,CAAC,QAAQ,CAAC;YAC1C,MAAM,OAAO,GAAG,MAAM,oBAAoB,CAAC,EAAE,CAAC;AAC9C,YAAA,UAAU,CAAC,SAAS,CAAC,OAAO,CAAC;AAC7B,YAAA,OAAO,OAAO;QAChB,CAAC;KACF;AACH;;AC/FA;;AAEG;AACH,MAAM,WAAW,GAAG,IAAI,GAAG,EAAkB;AAE7C;;;;AAIG;AACG,SAAU,QAAQ,CAAC,MAAc,EAAA;IACrC,MAAM,EAAE,GAAG,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;IACvC,WAAW,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,GAAG,CAAC,CAAC;AAC/B,IAAA,OAAO,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,EAAE,EAAE;AAC1B;;ACdA;;AAEG;AAEH;;;;AAIG;AACG,SAAU,QAAQ,CAAC,KAAc,EAAA;AACrC,IAAA,OAAO,OAAO,KAAK,KAAK,QAAQ;AAClC;AAEA;;;;AAIG;AACG,SAAU,QAAQ,CAAC,KAAc,EAAA;AACrC,IAAA,OAAO,OAAO,KAAK,KAAK,QAAQ;AAClC;AAEA;;;;AAIG;AACG,SAAU,SAAS,CAAC,KAAc,EAAA;AACtC,IAAA,OAAO,OAAO,KAAK,KAAK,SAAS;AACnC;AAEA;;;;AAIG;AACG,SAAU,UAAU,CAAC,KAAc,EAAA;AACvC,IAAA,OAAO,OAAO,KAAK,KAAK,UAAU;AACpC;AAEA;;;;AAIG;AACG,SAAU,QAAQ,CAAC,KAAc,EAAA;AACrC,IAAA,OAAO,CAAC,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;AACtE;AAEA;;;;AAIG;AACG,SAAU,WAAW,CAAC,KAAc,EAAA;AACxC,IAAA,OAAO,OAAO,KAAK,KAAK,WAAW;AACrC;AAEA;;;;AAIG;AACG,SAAU,KAAK,CAAC,KAAc,EAAA;IAClC,OAAO,WAAW,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,IAAI;AAC7C;AAEA;;;;AAIG;AACG,SAAU,MAAM,CAAI,KAA2B,EAAA;AACnD,IAAA,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC;AACtB;;ACxEA;;;;;;;AAOG;SACa,QAAQ,CACtB,MAAiB,EACjB,EAA2D,EAC3D,OAAgC,EAAA;AAEhC,IAAA,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,oFAAC;IAEtC,MAAM,CACJ,MAAK;AACH,QAAA,MAAM,KAAK,GAAG,MAAM,EAAE;AACtB,QAAA,IAAI,KAAK,KAAK,aAAa,EAAE,EAAE;AAC7B,YAAA,SAAS,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,CAAC,CAAC;AAC3C,YAAA,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC;QAC1B;IACF,CAAC,EACD,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,CAChC;;AAGD,IAAA,EAAE,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC;AACpB;AAEA;;;;;;AAMG;AACG,SAAU,eAAe,CAC7B,MAAuB,EACvB,MAAmB,EACnB,OAAoB,EACpB,OAAgC,EAAA;IAEhC,QAAQ,CAAC,MAAM,EAAE,KAAK,KAAK,KAAK,GAAG,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,EAAE,OAAO,CAAC;AACxE;;AC9CA;;AAEG;;;;"}
1
+ {"version":3,"file":"ng-primitives-utils.mjs","sources":["../../../../packages/ng-primitives/utils/src/forms/providers.ts","../../../../packages/ng-primitives/utils/src/observables/take-until-destroyed.ts","../../../../packages/ng-primitives/utils/src/forms/status.ts","../../../../packages/ng-primitives/utils/src/helpers/attributes.ts","../../../../packages/ng-primitives/utils/src/helpers/disposables.ts","../../../../packages/ng-primitives/utils/src/helpers/unique-id.ts","../../../../packages/ng-primitives/utils/src/helpers/validators.ts","../../../../packages/ng-primitives/utils/src/signals/index.ts","../../../../packages/ng-primitives/utils/src/ng-primitives-utils.ts"],"sourcesContent":["import { ExistingProvider, Type } from '@angular/core';\nimport { NG_VALUE_ACCESSOR } from '@angular/forms';\n\n/**\n * A simple helper function to provide a value accessor for a given type.\n * @param type The type to provide the value accessor for\n */\nexport function provideValueAccessor<T>(type: Type<T>): ExistingProvider {\n return { provide: NG_VALUE_ACCESSOR, useExisting: type, multi: true };\n}\n","/* eslint-disable @nx/workspace-take-until-destroyed */\nimport { DestroyRef } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { EMPTY, MonoTypeOperatorFunction, NEVER, pipe } from 'rxjs';\nimport { catchError, defaultIfEmpty, takeUntil } from 'rxjs/operators';\n\n/**\n * The built-in `takeUntilDestroyed` operator does not handle the case when the component is destroyed before the source observable emits.\n * This operator ensures that the source observable completes gracefully without throwing an error.\n * https://github.com/angular/angular/issues/54527#issuecomment-2098254508\n *\n * @internal\n */\nexport function safeTakeUntilDestroyed<T>(destroyRef?: DestroyRef): MonoTypeOperatorFunction<T> {\n return pipe(\n takeUntil(\n NEVER.pipe(\n takeUntilDestroyed(destroyRef),\n catchError(() => EMPTY),\n defaultIfEmpty(null),\n ),\n ),\n );\n}\n","import {\n DestroyRef,\n Injector,\n Signal,\n WritableSignal,\n effect,\n inject,\n signal,\n untracked,\n} from '@angular/core';\nimport { NgControl } from '@angular/forms';\nimport { onMount } from 'ng-primitives/state';\nimport { safeTakeUntilDestroyed } from '../observables/take-until-destroyed';\n\nexport interface NgpControlStatus {\n valid: boolean | null;\n invalid: boolean | null;\n pristine: boolean | null;\n dirty: boolean | null;\n touched: boolean | null;\n pending: boolean | null;\n disabled: boolean | null;\n}\n\n/**\n * Detects an Angular signal-forms interop control without importing any of the new\n * signal-form types. Interop controls expose a `field()` method which returns the\n * underlying FieldState.\n */\nfunction isInteropControl(control: NgControl | null | undefined): boolean {\n return !!control && typeof (control as any).field === 'function';\n}\n\n/**\n * Reads status from a control and updates the status signal.\n * Wrapped in try-catch to handle signal-forms interop controls where\n * the `field` input may not be available yet (throws NG0950).\n */\nfunction updateStatus(control: NgControl, status: WritableSignal<NgpControlStatus>): void {\n try {\n // For interop controls, read directly from the control (which has signal getters).\n // For classic controls, read from the underlying AbstractControl.\n const source = isInteropControl(control) ? control : ((control as any).control ?? control);\n\n const newStatus: NgpControlStatus = {\n valid: source.valid ?? null,\n invalid: source.invalid ?? null,\n pristine: source.pristine ?? null,\n dirty: source.dirty ?? null,\n touched: source.touched ?? null,\n pending: source.pending ?? null,\n disabled: source.disabled ?? null,\n };\n\n untracked(() => status.set(newStatus));\n } catch {\n // NG0950: Required input not available yet. The effect will re-run\n // when the signal input becomes available.\n }\n}\n\n/**\n * A utility function to get the status of an Angular form control as a reactive signal.\n * This function injects the NgControl and returns a signal that reflects the control's status.\n * It supports both classic reactive forms controls and signal-forms interop controls.\n * @internal\n */\n/**\n * Sets up event subscription for a given NgControl.\n * Only sets up the subscription - does not call updateStatus.\n */\nfunction setupEventSubscription(\n ngControl: NgControl,\n status: WritableSignal<NgpControlStatus>,\n destroyRef: DestroyRef,\n): void {\n // For classic controls, also subscribe to the events observable.\n const underlyingControl = (ngControl as any).control;\n if (underlyingControl?.events) {\n underlyingControl.events\n .pipe(safeTakeUntilDestroyed(destroyRef))\n .subscribe(() => updateStatus(ngControl, status));\n }\n}\n\nexport function controlStatus(): Signal<NgpControlStatus> {\n const injector = inject(Injector);\n const destroyRef = inject(DestroyRef);\n\n const status = signal<NgpControlStatus>({\n valid: null,\n invalid: null,\n pristine: null,\n dirty: null,\n touched: null,\n pending: null,\n disabled: null,\n });\n\n const control = signal<NgControl | null>(null);\n\n onMount(() => {\n // Try to inject NgControl immediately for initial state\n control.set(inject(NgControl, { optional: true }));\n\n // If we have a control immediately, update initial status\n if (control()) {\n updateStatus(control()!, status);\n }\n\n // Get the control (either from initial injection or from mount)\n const mountControl = control() || inject(NgControl, { optional: true });\n\n if (!mountControl) {\n return;\n }\n\n // Update control signal if it wasn't set before\n if (!control()) {\n control.set(mountControl);\n }\n\n // Update status to ensure latest values\n updateStatus(mountControl, status);\n\n // Set up event subscription for reactive updates\n setupEventSubscription(mountControl, status, destroyRef);\n });\n\n // Use an effect to reactively track status changes.\n // For signal-forms interop controls, the status properties are signals.\n // For classic controls, this will read the current values and establish\n // no signal dependencies, but we also subscribe to events below.\n effect(\n () => {\n const c = control();\n if (c) {\n updateStatus(c, status);\n }\n },\n { injector },\n );\n\n return status;\n}\n","import { afterRenderEffect, Signal } from '@angular/core';\n\nexport function booleanAttributeBinding(\n element: HTMLElement,\n attribute: string,\n value: Signal<boolean> | undefined,\n): void {\n // eslint-disable-next-line @angular-eslint/no-uncalled-signals -- checking whether the optional signal was provided, not its value\n if (value === undefined) {\n return;\n }\n\n afterRenderEffect({\n write: () =>\n value() ? element.setAttribute(attribute, '') : element.removeAttribute(attribute),\n });\n}\n","import { DestroyRef, inject } from '@angular/core';\n\n/**\n * Disposable functions are a way to manage timers, intervals, and event listeners\n * that should be cleared when a component is destroyed.\n *\n * Each disposable releases its DestroyRef registration as soon as the resource\n * is gone (the timer fires or the returned cleanup runs), so repeated calls -\n * e.g. rescheduling a timeout on every pointermove - don't accumulate destroy\n * callbacks for the lifetime of the host.\n *\n * This is heavily inspired by Headless UI disposables:\n * https://github.com/tailwindlabs/headlessui/blob/main/packages/%40headlessui-react/src/utils/disposables.ts\n */\nexport function injectDisposables() {\n const destroyRef = inject(DestroyRef);\n let isDestroyed = false;\n\n destroyRef.onDestroy(() => (isDestroyed = true));\n\n return {\n /**\n * Set a timeout that will be cleared when the component is destroyed.\n * @param callback The callback to execute\n * @param delay The delay before the callback is executed\n * @returns A function to clear the timeout\n */\n setTimeout: (callback: () => void, delay: number) => {\n if (isDestroyed) {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {};\n }\n\n const id = setTimeout(() => {\n unregister();\n callback();\n }, delay);\n const unregister = destroyRef.onDestroy(() => clearTimeout(id));\n return () => {\n clearTimeout(id);\n unregister();\n };\n },\n /**\n * Add an event listener that will be removed when the component is destroyed.\n * @param target The event target\n * @param type The event type\n * @param listener The event listener\n * @param options The event listener options\n * @returns A function to remove the event listener\n */\n addEventListener: <K extends keyof HTMLElementEventMap>(\n target: EventTarget,\n type: K,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any,\n options?: boolean | AddEventListenerOptions,\n ) => {\n if (isDestroyed) {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {};\n }\n\n target.addEventListener(type, listener as EventListenerOrEventListenerObject, options);\n const unregister = destroyRef.onDestroy(() =>\n target.removeEventListener(type, listener as EventListenerOrEventListenerObject, options),\n );\n return () => {\n target.removeEventListener(type, listener as EventListenerOrEventListenerObject, options);\n unregister();\n };\n },\n /**\n * Set an interval that will be cleared when the component is destroyed.\n * @param callback The callback to execute\n * @param delay The delay between executions\n * @returns A function to clear the interval\n */\n setInterval: (callback: () => void, delay: number) => {\n if (isDestroyed) {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {};\n }\n\n const id = setInterval(callback, delay);\n const unregister = destroyRef.onDestroy(() => clearInterval(id));\n return () => {\n clearInterval(id);\n unregister();\n };\n },\n /**\n * Set a requestAnimationFrame that will be cleared when the component is destroyed.\n * @param callback The callback to execute\n * @returns A function to clear the requestAnimationFrame\n */\n requestAnimationFrame: (callback: FrameRequestCallback) => {\n if (isDestroyed) {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {};\n }\n\n const id = requestAnimationFrame(time => {\n unregister();\n callback(time);\n });\n const unregister = destroyRef.onDestroy(() => cancelAnimationFrame(id));\n return () => {\n cancelAnimationFrame(id);\n unregister();\n };\n },\n };\n}\n","/**\n * Store a map of unique ids for elements so that there are no collisions.\n */\nconst uniqueIdMap = new Map<string, number>();\n\n/**\n * Generate a unique id for an element\n * @param prefix - The prefix to use for the id\n * @returns The generated id\n */\nexport function uniqueId(prefix: string): string {\n const id = uniqueIdMap.get(prefix) ?? 0;\n uniqueIdMap.set(prefix, id + 1);\n return `${prefix}-${id}`;\n}\n","/**\n * Type validation utilities\n */\n\n/**\n * Checks if a value is a string\n * @param value - The value to check\n * @returns true if the value is a string, false otherwise\n */\nexport function isString(value: unknown): value is string {\n return typeof value === 'string';\n}\n\n/**\n * Checks if a value is a number\n * @param value - The value to check\n * @returns true if the value is a number, false otherwise\n */\nexport function isNumber(value: unknown): value is number {\n return typeof value === 'number';\n}\n\n/**\n * Checks if a value is a boolean\n * @param value - The value to check\n * @returns true if the value is a boolean, false otherwise\n */\nexport function isBoolean(value: unknown): value is boolean {\n return typeof value === 'boolean';\n}\n\n/**\n * Checks if a value is a function\n * @param value - The value to check\n * @returns true if the value is a function, false otherwise\n */\nexport function isFunction(value: unknown): value is CallableFunction {\n return typeof value === 'function';\n}\n\n/**\n * Checks if a value is a plain object (but not null or array)\n * @param value - The value to check\n * @returns true if the value is a plain object, false otherwise\n */\nexport function isObject(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === 'object' && !Array.isArray(value);\n}\n\n/**\n * Checks if a value is undefined\n * @param value - The value to check\n * @returns true if the value is undefined, false otherwise\n */\nexport function isUndefined(value: unknown): value is undefined {\n return typeof value === 'undefined';\n}\n\n/**\n * Checks if a value is null or undefined\n * @param value - The value to check\n * @returns true if the value is null or undefined, false otherwise\n */\nexport function isNil(value: unknown): value is null | undefined {\n return isUndefined(value) || value === null;\n}\n\n/**\n * Checks if a value is not null and not undefined\n * @param value - The value to check\n * @returns true if the value is not null and not undefined, false otherwise\n */\nexport function notNil<T>(value: T | null | undefined): value is T {\n return !isNil(value);\n}\n","import { effect, Injector, Signal, signal, untracked } from '@angular/core';\n\n/**\n * Listen for changes to a signal and call a function when the signal changes.\n * @param source\n * @param fn\n * @param options\n * @param options.injector\n * @internal\n */\nexport function onChange<T>(\n source: Signal<T>,\n fn: (value: T, previousValue: T | null | undefined) => void,\n options?: { injector: Injector },\n): void {\n const previousValue = signal(source());\n\n effect(\n () => {\n const value = source();\n if (value !== previousValue()) {\n untracked(() => fn(value, previousValue()));\n previousValue.set(value);\n }\n },\n { injector: options?.injector },\n );\n\n // call the fn with the initial value\n fn(source(), null);\n}\n\n/**\n * Listen for changes to a boolean signal and call one of two functions when the signal changes.\n * @param source\n * @param onTrue\n * @param onFalse\n * @param options\n */\nexport function onBooleanChange(\n source: Signal<boolean>,\n onTrue?: () => void,\n onFalse?: () => void,\n options?: { injector: Injector },\n): void {\n onChange(source, value => (value ? onTrue?.() : onFalse?.()), options);\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "ng-primitives",
3
3
  "description": "Angular Primitives is a low-level headless UI component library with a focus on accessibility, customization, and developer experience. ",
4
4
  "license": "Apache-2.0",
5
- "version": "0.122.0",
5
+ "version": "0.123.0",
6
6
  "keywords": [
7
7
  "angular",
8
8
  "primitives",
@@ -57,8 +57,8 @@ export class ToggleGroup<%= componentSuffix %> implements ControlValueAccessor {
57
57
  }
58
58
 
59
59
  /** Write a new value to the toggle group. */
60
- writeValue(value: string[]): void {
61
- this.toggleGroup().setValue(value, { emit: false });
60
+ writeValue(value: string[] | null): void {
61
+ this.toggleGroup().setValue(value ?? [], { emit: false });
62
62
  }
63
63
 
64
64
  /** Register a callback function to be called when the value changes. */
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { OnDestroy, ClassProvider, EffectCleanupRegisterFn, CreateEffectOptions, EffectRef, ElementRef, Signal, Injector } from '@angular/core';
2
+ import { OnDestroy, ClassProvider, Signal, EffectCleanupRegisterFn, CreateEffectOptions, EffectRef, ElementRef, Injector } from '@angular/core';
3
3
  import { Observable } from 'rxjs';
4
4
 
5
5
  declare class NgpExitAnimation implements OnDestroy {
@@ -45,6 +45,98 @@ declare class NgpExitAnimationManager {
45
45
  declare function provideExitAnimationManager(): ClassProvider;
46
46
  declare function injectExitAnimationManager(): NgpExitAnimationManager;
47
47
 
48
+ /**
49
+ * Fallback timeout (ms) after which an idle hover bridge closes the overlay.
50
+ * Shared so tooltip, menu and submenu hover-intent behaviour stay in sync.
51
+ */
52
+ declare const HOVER_BRIDGE_TIMEOUT_MS = 150;
53
+ /**
54
+ * Pointer movement below this magnitude (px) on the intent axis is treated as
55
+ * jitter, not a reversal, so a tiny backward twitch doesn't collapse the bridge.
56
+ */
57
+ declare const HOVER_BRIDGE_DIRECTION_TOLERANCE_PX = 2;
58
+ interface HoverBridgePoint {
59
+ x: number;
60
+ y: number;
61
+ }
62
+ /**
63
+ * The dominant axis and sign pointing from the trigger toward the target. Used
64
+ * to reject pointer movement heading away from the target while inside the corridor.
65
+ */
66
+ interface HoverBridgeDirection {
67
+ axis: 'x' | 'y';
68
+ sign: 1 | -1;
69
+ }
70
+ interface CreateHoverBridgePolygonOptions {
71
+ triggerRect: DOMRect | null;
72
+ targetRect: DOMRect | null;
73
+ exitPoint: HoverBridgePoint;
74
+ corridorHalfSize?: number;
75
+ }
76
+ /**
77
+ * Computes the dominant axis and sign from the trigger toward the target - i.e.
78
+ * which way the pointer must travel to reach the panel. Returns null if either
79
+ * rect is missing.
80
+ */
81
+ declare function getHoverBridgeDirection(triggerRect: DOMRect | null, targetRect: DOMRect | null): HoverBridgeDirection | null;
82
+ /**
83
+ * Builds a pointer grace polygon between the trigger exit point and the target overlay.
84
+ * The polygon is intentionally directional so moving away from the target exits quickly.
85
+ */
86
+ declare function createHoverBridgePolygon({ triggerRect, targetRect, exitPoint, corridorHalfSize, }: CreateHoverBridgePolygonOptions): HoverBridgePoint[] | null;
87
+ /**
88
+ * Returns true when the point lies inside the provided polygon.
89
+ */
90
+ declare function isPointInHoverBridgePolygon(point: HoverBridgePoint, polygon: HoverBridgePoint[]): boolean;
91
+
92
+ interface HoverBridgeOptions {
93
+ /** Whether the pointer is currently over the trigger or the panel (the "safe" area). */
94
+ isPointerInAnchor: () => boolean;
95
+ /** Close the overlay - called when the pointer leaves the corridor or the idle timer fires. */
96
+ close: () => void;
97
+ /**
98
+ * Require the pointer to keep heading toward the panel. When true, reversing
99
+ * away along the corridor's dominant axis closes the overlay (menu/submenu).
100
+ * Tooltips leave this off. Defaults to false.
101
+ */
102
+ requireForwardMovement?: boolean;
103
+ /**
104
+ * Reset the idle-fallback timer on valid in-corridor movement, so it only
105
+ * fires after the pointer genuinely stops. When false the timer is a fixed cap
106
+ * from the moment the corridor is built (tooltip's original semantics).
107
+ * Defaults to true.
108
+ */
109
+ resetFallbackOnMove?: boolean;
110
+ /** Idle-fallback timeout in ms. Defaults to HOVER_BRIDGE_TIMEOUT_MS. */
111
+ timeoutMs?: number;
112
+ }
113
+ interface HoverBridgeTrackOptions {
114
+ triggerRect: DOMRect | null;
115
+ targetRect: DOMRect | null;
116
+ exitPoint: HoverBridgePoint;
117
+ }
118
+ interface HoverBridgeController {
119
+ /** The active corridor polygon, or null when no bridge is in progress. */
120
+ readonly polygon: Signal<HoverBridgePoint[] | null>;
121
+ /** Whether a corridor is currently active. */
122
+ isActive(): boolean;
123
+ /**
124
+ * Build a corridor from the exit point toward the panel and start tracking the
125
+ * pointer. Returns false (and does nothing) when a polygon can't be built, so
126
+ * the caller can apply its own fallback.
127
+ */
128
+ track(options: HoverBridgeTrackOptions): boolean;
129
+ /** Tear down the corridor and its global listener/timer. */
130
+ clear(): void;
131
+ }
132
+ /**
133
+ * Shared safe-polygon hover-intent state machine used by the menu, submenu and
134
+ * tooltip triggers. While the pointer travels inside the corridor toward the
135
+ * panel the overlay stays open; it closes when the pointer leaves the corridor,
136
+ * reverses away (when requireForwardMovement is set) or idles past the timeout.
137
+ */
138
+ declare function createHoverBridge({ isPointerInAnchor, close, requireForwardMovement, resetFallbackOnMove, timeoutMs, }: HoverBridgeOptions): HoverBridgeController;
139
+
48
140
  /**
49
141
  * This implementation is heavily inspired by the great work on ngextension!
50
142
  * https://github.com/ngxtension/ngxtension-platform/blob/main/libs/ngxtension/explicit-effect/src/explicit-effect.ts
@@ -210,5 +302,5 @@ declare function injectDimensions(): Signal<Dimensions>;
210
302
 
211
303
  declare function scrollIntoViewIfNeeded(element: HTMLElement): void;
212
304
 
213
- export { NgpExitAnimation, NgpExitAnimationManager, StyleInjector, domSort, explicitEffect, fromMutationObserver, fromResizeEvent, injectDimensions, injectElementRef, injectExitAnimationManager, injectStyleInjector, observeResize, onDomRemoval, provideExitAnimationManager, scrollIntoViewIfNeeded, setupExitAnimation, setupOverflowListener };
214
- export type { Dimensions, NgpExitAnimationRef };
305
+ export { HOVER_BRIDGE_DIRECTION_TOLERANCE_PX, HOVER_BRIDGE_TIMEOUT_MS, NgpExitAnimation, NgpExitAnimationManager, StyleInjector, createHoverBridge, createHoverBridgePolygon, domSort, explicitEffect, fromMutationObserver, fromResizeEvent, getHoverBridgeDirection, injectDimensions, injectElementRef, injectExitAnimationManager, injectStyleInjector, isPointInHoverBridgePolygon, observeResize, onDomRemoval, provideExitAnimationManager, scrollIntoViewIfNeeded, setupExitAnimation, setupOverflowListener };
306
+ export type { Dimensions, HoverBridgeController, HoverBridgeDirection, HoverBridgeOptions, HoverBridgePoint, HoverBridgeTrackOptions, NgpExitAnimationRef };
@@ -653,6 +653,11 @@ declare class NgpSubmenuTrigger<T = unknown> {
653
653
  * @default true
654
654
  */
655
655
  readonly flip: _angular_core.InputSignalWithTransform<NgpFlip, NgpFlipInput>;
656
+ /**
657
+ * Define the container in which the menu should be attached.
658
+ * @default document.body
659
+ */
660
+ readonly container: _angular_core.InputSignal<string | HTMLElement | null>;
656
661
  /**
657
662
  * Access the menu trigger state.
658
663
  */
@@ -676,7 +681,7 @@ declare class NgpSubmenuTrigger<T = unknown> {
676
681
  */
677
682
  focus(origin?: FocusOrigin): void;
678
683
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<NgpSubmenuTrigger<any>, never>;
679
- static ɵdir: _angular_core.ɵɵDirectiveDeclaration<NgpSubmenuTrigger<any>, "[ngpSubmenuTrigger]", ["ngpSubmenuTrigger"], { "menu": { "alias": "ngpSubmenuTrigger"; "required": false; "isSignal": true; }; "disabled": { "alias": "ngpSubmenuTriggerDisabled"; "required": false; "isSignal": true; }; "placement": { "alias": "ngpSubmenuTriggerPlacement"; "required": false; "isSignal": true; }; "offset": { "alias": "ngpSubmenuTriggerOffset"; "required": false; "isSignal": true; }; "flip": { "alias": "ngpSubmenuTriggerFlip"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
684
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<NgpSubmenuTrigger<any>, "[ngpSubmenuTrigger]", ["ngpSubmenuTrigger"], { "menu": { "alias": "ngpSubmenuTrigger"; "required": false; "isSignal": true; }; "disabled": { "alias": "ngpSubmenuTriggerDisabled"; "required": false; "isSignal": true; }; "placement": { "alias": "ngpSubmenuTriggerPlacement"; "required": false; "isSignal": true; }; "offset": { "alias": "ngpSubmenuTriggerOffset"; "required": false; "isSignal": true; }; "flip": { "alias": "ngpSubmenuTriggerFlip"; "required": false; "isSignal": true; }; "container": { "alias": "ngpSubmenuTriggerContainer"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
680
685
  }
681
686
 
682
687
  interface NgpSubmenuTriggerState {
@@ -704,6 +709,11 @@ interface NgpSubmenuTriggerState {
704
709
  * Whether the menu should flip when there is not enough space.
705
710
  */
706
711
  readonly flip: WritableSignal<NgpFlip>;
712
+ /**
713
+ * The container in which the menu should be attached.
714
+ * @default document.body
715
+ */
716
+ readonly container: WritableSignal<HTMLElement | string | null>;
707
717
  /**
708
718
  * The focus origin used to open the submenu.
709
719
  * Used by the submenu's focus trap for :focus-visible styling.
@@ -750,14 +760,20 @@ interface NgpSubmenuTriggerState {
750
760
  * @param shouldFlip - Whether the menu should flip
751
761
  */
752
762
  setFlip(shouldFlip: NgpFlip): void;
763
+ /**
764
+ * Set the container in which the menu should be attached. Takes effect the
765
+ * next time the menu is opened; it does not move a menu that is already open.
766
+ * @param container - The new container
767
+ */
768
+ setContainer(container: HTMLElement | string | null): void;
753
769
  /**
754
770
  * Focus the trigger element.
755
771
  * @param origin - The focus origin
756
772
  */
757
773
  focus(origin: FocusOrigin): void;
758
774
  /**
759
- * Set whether the pointer is over the menu content.
760
- * For submenus, this is a no-op as hover is handled via showSubmenuOnHover.
775
+ * Set whether the pointer is over the submenu content. Entering the submenu
776
+ * tears down the hover bridge (the pointer arrived safely).
761
777
  * @param isOver - Whether the pointer is over the content
762
778
  * @internal
763
779
  */
@@ -784,6 +800,10 @@ interface NgpSubmenuTriggerProps<T = unknown> {
784
800
  * Whether the menu should flip when there is not enough space.
785
801
  */
786
802
  readonly flip?: Signal<NgpFlip>;
803
+ /**
804
+ * The container in which the menu should be attached.
805
+ */
806
+ readonly container?: Signal<HTMLElement | string | null>;
787
807
  }
788
808
  declare const NgpSubmenuTriggerStateToken: _angular_core.InjectionToken<WritableSignal<{
789
809
  placement: WritableSignal<NgpMenuPlacement>;
@@ -801,10 +821,12 @@ declare const NgpSubmenuTriggerStateToken: _angular_core.InjectionToken<Writable
801
821
  setFlip: (shouldFlip: NgpFlip) => void;
802
822
  setPlacement: (newPlacement: NgpMenuPlacement) => void;
803
823
  setOffset: (newOffset: NgpOffset) => void;
824
+ setContainer: (newContainer: HTMLElement | string | null) => void;
804
825
  focus: (origin: FocusOrigin) => void;
805
- setPointerOverContent: (_isOver: boolean) => void;
826
+ setPointerOverContent: (isOver: boolean) => void;
827
+ container: WritableSignal<string | HTMLElement | null>;
806
828
  }>>;
807
- declare const ngpSubmenuTrigger: <T>({ disabled: _disabled, menu: _menu, placement: _placement, offset: _offset, flip: _flip, }: NgpSubmenuTriggerProps<T>) => {
829
+ declare const ngpSubmenuTrigger: <T>({ disabled: _disabled, menu: _menu, placement: _placement, offset: _offset, flip: _flip, container: _container, }: NgpSubmenuTriggerProps<T>) => {
808
830
  placement: WritableSignal<NgpMenuPlacement>;
809
831
  offset: WritableSignal<NgpOffset>;
810
832
  disabled: WritableSignal<boolean>;
@@ -820,8 +842,10 @@ declare const ngpSubmenuTrigger: <T>({ disabled: _disabled, menu: _menu, placeme
820
842
  setFlip: (shouldFlip: NgpFlip) => void;
821
843
  setPlacement: (newPlacement: NgpMenuPlacement) => void;
822
844
  setOffset: (newOffset: NgpOffset) => void;
845
+ setContainer: (newContainer: HTMLElement | string | null) => void;
823
846
  focus: (origin: FocusOrigin) => void;
824
- setPointerOverContent: (_isOver: boolean) => void;
847
+ setPointerOverContent: (isOver: boolean) => void;
848
+ container: WritableSignal<string | HTMLElement | null>;
825
849
  };
826
850
  declare const injectSubmenuTriggerState: {
827
851
  (): Signal<{
@@ -840,8 +864,10 @@ declare const injectSubmenuTriggerState: {
840
864
  setFlip: (shouldFlip: NgpFlip) => void;
841
865
  setPlacement: (newPlacement: NgpMenuPlacement) => void;
842
866
  setOffset: (newOffset: NgpOffset) => void;
867
+ setContainer: (newContainer: HTMLElement | string | null) => void;
843
868
  focus: (origin: FocusOrigin) => void;
844
- setPointerOverContent: (_isOver: boolean) => void;
869
+ setPointerOverContent: (isOver: boolean) => void;
870
+ container: WritableSignal<string | HTMLElement | null>;
845
871
  }>;
846
872
  (options: ng_primitives_state.StateInjectionOptions): Signal<{
847
873
  placement: WritableSignal<NgpMenuPlacement>;
@@ -859,8 +885,10 @@ declare const injectSubmenuTriggerState: {
859
885
  setFlip: (shouldFlip: NgpFlip) => void;
860
886
  setPlacement: (newPlacement: NgpMenuPlacement) => void;
861
887
  setOffset: (newOffset: NgpOffset) => void;
888
+ setContainer: (newContainer: HTMLElement | string | null) => void;
862
889
  focus: (origin: FocusOrigin) => void;
863
- setPointerOverContent: (_isOver: boolean) => void;
890
+ setPointerOverContent: (isOver: boolean) => void;
891
+ container: WritableSignal<string | HTMLElement | null>;
864
892
  } | null>;
865
893
  (options?: ng_primitives_state.StateInjectionOptions): Signal<{
866
894
  placement: WritableSignal<NgpMenuPlacement>;
@@ -878,8 +906,10 @@ declare const injectSubmenuTriggerState: {
878
906
  setFlip: (shouldFlip: NgpFlip) => void;
879
907
  setPlacement: (newPlacement: NgpMenuPlacement) => void;
880
908
  setOffset: (newOffset: NgpOffset) => void;
909
+ setContainer: (newContainer: HTMLElement | string | null) => void;
881
910
  focus: (origin: FocusOrigin) => void;
882
- setPointerOverContent: (_isOver: boolean) => void;
911
+ setPointerOverContent: (isOver: boolean) => void;
912
+ container: WritableSignal<string | HTMLElement | null>;
883
913
  }> | Signal<{
884
914
  placement: WritableSignal<NgpMenuPlacement>;
885
915
  offset: WritableSignal<NgpOffset>;
@@ -896,8 +926,10 @@ declare const injectSubmenuTriggerState: {
896
926
  setFlip: (shouldFlip: NgpFlip) => void;
897
927
  setPlacement: (newPlacement: NgpMenuPlacement) => void;
898
928
  setOffset: (newOffset: NgpOffset) => void;
929
+ setContainer: (newContainer: HTMLElement | string | null) => void;
899
930
  focus: (origin: FocusOrigin) => void;
900
- setPointerOverContent: (_isOver: boolean) => void;
931
+ setPointerOverContent: (isOver: boolean) => void;
932
+ container: WritableSignal<string | HTMLElement | null>;
901
933
  } | null>;
902
934
  };
903
935
  declare const provideSubmenuTriggerState: (opts?: {
@@ -102,6 +102,12 @@ interface CooldownOverlay {
102
102
  hideImmediate(): void;
103
103
  /** Optional signal to mark the transition as instant due to cooldown */
104
104
  instantTransition?: WritableSignal<boolean>;
105
+ /**
106
+ * Optional check for whether this overlay is a descendant of the given overlay
107
+ * (i.e. its trigger is rendered within the other overlay's content). Used to
108
+ * keep an ancestor open when a nested overlay of the same type is activated.
109
+ */
110
+ isDescendantOf?(other: CooldownOverlay): boolean;
105
111
  }
106
112
  /**
107
113
  * Singleton service that tracks close timestamps and active overlays per overlay type.
@@ -112,6 +118,13 @@ interface CooldownOverlay {
112
118
  */
113
119
  declare class NgpOverlayCooldownManager {
114
120
  private readonly lastCloseTimestamps;
121
+ /**
122
+ * Active overlays per type, stored as a stack ordered oldest-first / topmost-last.
123
+ * Most types only ever hold a single overlay, but when an overlay is nested inside
124
+ * another of the same type (e.g. a popover whose trigger lives inside another
125
+ * popover) the child is pushed on top of its ancestor instead of evicting it.
126
+ * Closing the child then restores the ancestor as the active overlay.
127
+ */
115
128
  private readonly activeOverlays;
116
129
  /**
117
130
  * Record the close timestamp for an overlay type.
@@ -127,7 +140,14 @@ declare class NgpOverlayCooldownManager {
127
140
  isWithinCooldown(overlayType: string, duration: number): boolean;
128
141
  /**
129
142
  * Register an overlay as active for its type.
130
- * Any existing overlay of the same type will be closed immediately.
143
+ *
144
+ * Any existing overlay of the same type is closed immediately so that only one
145
+ * overlay of each type is open at a time - *unless* it is an ancestor of the
146
+ * overlay being registered. A nested overlay (its trigger rendered inside an
147
+ * ancestor overlay's content) is stacked on top of its ancestor instead of
148
+ * evicting it, allowing legitimate nesting to coexist while sibling overlays
149
+ * still replace one another.
150
+ *
131
151
  * @param overlayType The type identifier for the overlay group
132
152
  * @param overlay The overlay instance
133
153
  * @param cooldown The cooldown duration - if > 0, enables instant transitions
@@ -629,6 +649,16 @@ declare class NgpOverlay<T = unknown> implements CooldownOverlay {
629
649
  * @internal
630
650
  */
631
651
  unregisterChildOverlay(child: NgpOverlay): void;
652
+ /**
653
+ * Determine whether this overlay is a descendant of the given overlay - i.e.
654
+ * its trigger is rendered within the other overlay's content. Walks the parent
655
+ * overlay chain established through dependency injection.
656
+ *
657
+ * Used by the cooldown manager to avoid evicting an ancestor overlay when a
658
+ * nested overlay of the same type is activated.
659
+ * @internal
660
+ */
661
+ isDescendantOf(other: CooldownOverlay): boolean;
632
662
  /**
633
663
  * Check if the event path includes any child overlay elements (recursively).
634
664
  * @internal
@@ -7,6 +7,8 @@ import { Placement } from '@floating-ui/dom';
7
7
  import { NumberInput, BooleanInput } from '@angular/cdk/coercion';
8
8
  import * as ng_primitives_state from 'ng-primitives/state';
9
9
  import { StateInjectionOptions } from 'ng-primitives/state';
10
+ import * as ng_primitives_internal from 'ng-primitives/internal';
11
+ import { HoverBridgePoint } from 'ng-primitives/internal';
10
12
 
11
13
  interface NgpTooltipConfig {
12
14
  /**
@@ -115,11 +117,6 @@ declare const provideTooltipArrowState: (opts?: {
115
117
  inherit?: boolean;
116
118
  }) => _angular_core.FactoryProvider;
117
119
 
118
- interface TooltipHoverBridgePoint {
119
- x: number;
120
- y: number;
121
- }
122
-
123
120
  interface NgpTooltipTriggerState<T> {
124
121
  /** Access the tooltip template ref. */
125
122
  readonly tooltip: WritableSignal<NgpOverlayContent<T> | string | null>;
@@ -240,7 +237,7 @@ interface NgpTooltipTriggerState<T> {
240
237
  /**
241
238
  * Current pointer grace polygon used while crossing trigger -> tooltip.
242
239
  */
243
- readonly hoverBridgePolygon: Signal<TooltipHoverBridgePoint[] | null>;
240
+ readonly hoverBridgePolygon: Signal<HoverBridgePoint[] | null>;
244
241
  /**
245
242
  * Show the tooltip programmatically (skips cooldown so multiple tooltips can coexist).
246
243
  */
@@ -392,7 +389,7 @@ declare const NgpTooltipTriggerStateToken: _angular_core.InjectionToken<Writable
392
389
  open: Signal<boolean>;
393
390
  hasOverflow: Signal<boolean>;
394
391
  contentHovered: WritableSignal<boolean>;
395
- hoverBridgePolygon: WritableSignal<TooltipHoverBridgePoint[] | null>;
392
+ hoverBridgePolygon: Signal<HoverBridgePoint[] | null>;
396
393
  show: () => void;
397
394
  hide: () => void;
398
395
  setTooltipId: (id: string) => void;
@@ -425,7 +422,7 @@ declare const ngpTooltipTrigger: <T>({ tooltip: _tooltip, disabled, placement, o
425
422
  open: Signal<boolean>;
426
423
  hasOverflow: Signal<boolean>;
427
424
  contentHovered: WritableSignal<boolean>;
428
- hoverBridgePolygon: WritableSignal<TooltipHoverBridgePoint[] | null>;
425
+ hoverBridgePolygon: Signal<HoverBridgePoint[] | null>;
429
426
  show: () => void;
430
427
  hide: () => void;
431
428
  setTooltipId: (id: string) => void;
@@ -569,7 +566,7 @@ declare class NgpTooltipTrigger<T = null> implements OnDestroy {
569
566
  open: _angular_core.Signal<boolean>;
570
567
  hasOverflow: _angular_core.Signal<boolean>;
571
568
  contentHovered: _angular_core.WritableSignal<boolean>;
572
- hoverBridgePolygon: _angular_core.WritableSignal<TooltipHoverBridgePoint[] | null>;
569
+ hoverBridgePolygon: _angular_core.Signal<ng_primitives_internal.HoverBridgePoint[] | null>;
573
570
  show: () => void;
574
571
  hide: () => void;
575
572
  setTooltipId: (id: string) => void;
@@ -33,6 +33,11 @@ declare function booleanAttributeBinding(element: HTMLElement, attribute: string
33
33
  * Disposable functions are a way to manage timers, intervals, and event listeners
34
34
  * that should be cleared when a component is destroyed.
35
35
  *
36
+ * Each disposable releases its DestroyRef registration as soon as the resource
37
+ * is gone (the timer fires or the returned cleanup runs), so repeated calls -
38
+ * e.g. rescheduling a timeout on every pointermove - don't accumulate destroy
39
+ * callbacks for the lifetime of the host.
40
+ *
36
41
  * This is heavily inspired by Headless UI disposables:
37
42
  * https://github.com/tailwindlabs/headlessui/blob/main/packages/%40headlessui-react/src/utils/disposables.ts
38
43
  */
@@ -45,20 +50,18 @@ declare function injectDisposables(): {
45
50
  */
46
51
  setTimeout: (callback: () => void, delay: number) => () => void;
47
52
  /**
48
- * Set an interval that will be cleared when the component is destroyed.
49
- * @param callback The callback to execute
50
- * @param delay The delay before the callback is executed
51
- * @param target
52
- * @param type
53
- * @param listener
54
- * @param options
55
- * @returns A function to clear the interval
53
+ * Add an event listener that will be removed when the component is destroyed.
54
+ * @param target The event target
55
+ * @param type The event type
56
+ * @param listener The event listener
57
+ * @param options The event listener options
58
+ * @returns A function to remove the event listener
56
59
  */
57
60
  addEventListener: <K extends keyof HTMLElementEventMap>(target: EventTarget, type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions) => () => void;
58
61
  /**
59
62
  * Set an interval that will be cleared when the component is destroyed.
60
63
  * @param callback The callback to execute
61
- * @param delay The delay before the callback is executed
64
+ * @param delay The delay between executions
62
65
  * @returns A function to clear the interval
63
66
  */
64
67
  setInterval: (callback: () => void, delay: number) => () => void;