ng-primitives 0.110.1 → 0.111.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.
- package/fesm2022/ng-primitives-focus-trap.mjs +20 -2
- package/fesm2022/ng-primitives-focus-trap.mjs.map +1 -1
- package/fesm2022/ng-primitives-menu.mjs +209 -9
- package/fesm2022/ng-primitives-menu.mjs.map +1 -1
- package/fesm2022/ng-primitives-tooltip.mjs +2 -2
- package/fesm2022/ng-primitives-tooltip.mjs.map +1 -1
- package/menu/index.d.ts +186 -4
- package/package.json +1 -1
|
@@ -133,6 +133,9 @@ const [NgpFocusTrapStateToken, ngpFocusTrap, injectFocusTrapState, provideFocusT
|
|
|
133
133
|
if (element.nativeElement.contains(target)) {
|
|
134
134
|
lastFocusedElement = target;
|
|
135
135
|
}
|
|
136
|
+
else if (isAllowedExternalTarget(target)) {
|
|
137
|
+
// Don't interfere — see `isAllowedExternalTarget` for details.
|
|
138
|
+
}
|
|
136
139
|
else {
|
|
137
140
|
focus(lastFocusedElement);
|
|
138
141
|
}
|
|
@@ -145,10 +148,25 @@ const [NgpFocusTrapStateToken, ngpFocusTrap, injectFocusTrapState, provideFocusT
|
|
|
145
148
|
return;
|
|
146
149
|
}
|
|
147
150
|
const relatedTarget = event.relatedTarget;
|
|
148
|
-
if (!element.nativeElement.contains(relatedTarget)
|
|
151
|
+
if (!element.nativeElement.contains(relatedTarget) &&
|
|
152
|
+
!isAllowedExternalTarget(relatedTarget)) {
|
|
149
153
|
focus(lastFocusedElement);
|
|
150
154
|
}
|
|
151
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Whether the given element belongs to another focus trap or a CDK overlay container.
|
|
158
|
+
*
|
|
159
|
+
* When focus moves to these elements we must not redirect it back, because they
|
|
160
|
+
* manage their own focus trapping. Without this check, opening a CDK overlay
|
|
161
|
+
* (e.g. a select dropdown) from inside a focus-trapped dialog would immediately
|
|
162
|
+
* yank focus back into the dialog, making the overlay unusable.
|
|
163
|
+
*
|
|
164
|
+
* See https://github.com/nicecod3r/ng-primitives/issues/682
|
|
165
|
+
* and https://github.com/nicecod3r/ng-primitives/issues/687
|
|
166
|
+
*/
|
|
167
|
+
function isAllowedExternalTarget(target) {
|
|
168
|
+
return !!(target?.closest?.('[data-focus-trap]') || target?.closest?.('.cdk-overlay-container'));
|
|
169
|
+
}
|
|
152
170
|
/**
|
|
153
171
|
* If the focused element gets removed from the DOM, browsers move focus back to the document.body.
|
|
154
172
|
* We move focus to the container to keep focus trapped correctly.
|
|
@@ -214,7 +232,7 @@ const [NgpFocusTrapStateToken, ngpFocusTrap, injectFocusTrapState, provideFocusT
|
|
|
214
232
|
function getTabbableCandidates(container) {
|
|
215
233
|
const nodes = [];
|
|
216
234
|
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
|
|
217
|
-
acceptNode: (node) => interactivityChecker.
|
|
235
|
+
acceptNode: (node) => interactivityChecker.isTabbable(node)
|
|
218
236
|
? NodeFilter.FILTER_ACCEPT
|
|
219
237
|
: NodeFilter.FILTER_SKIP,
|
|
220
238
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ng-primitives-focus-trap.mjs","sources":["../../../../packages/ng-primitives/focus-trap/src/focus-trap/focus-trap-state.ts","../../../../packages/ng-primitives/focus-trap/src/focus-trap/focus-trap.ts","../../../../packages/ng-primitives/focus-trap/src/ng-primitives-focus-trap.ts"],"sourcesContent":["import { FocusMonitor, FocusOrigin, InteractivityChecker } from '@angular/cdk/a11y';\nimport { afterNextRender, inject, Injector, NgZone, signal, Signal } from '@angular/core';\nimport { injectElementRef } from 'ng-primitives/internal';\nimport { NgpOverlay } from 'ng-primitives/portal';\nimport {\n attrBinding,\n createPrimitive,\n dataBinding,\n listener,\n onDestroy,\n} from 'ng-primitives/state';\nimport { safeTakeUntilDestroyed } from 'ng-primitives/utils';\n\nclass FocusTrap {\n /**\n * Whether the focus trap is active.\n */\n active: boolean = false;\n\n /**\n * Activates the focus trap.\n */\n activate(): void {\n this.active = true;\n }\n\n /**\n * Deactivates the focus trap.\n */\n deactivate(): void {\n this.active = false;\n }\n}\n\nclass FocusTrapStack {\n /**\n * The stack of focus traps.\n */\n private readonly stack: FocusTrap[] = [];\n\n /**\n * Adds a focus trap to the stack.\n */\n add(focusTrap: FocusTrap): void {\n // deactivate the previous focus trap\n this.stack.forEach(t => t.deactivate());\n\n // add the new focus trap and activate it\n this.stack.push(focusTrap);\n focusTrap.activate();\n }\n\n /**\n * Removes a focus trap from the stack.\n */\n remove(focusTrap: FocusTrap): void {\n // remove the focus trap\n const index = this.stack.indexOf(focusTrap);\n\n if (index < 0) {\n return;\n }\n\n // Only reactivate the previous trap if we're removing from the top of the stack.\n // This prevents reactivating a parent trap when closing all overlays (where the\n // parent is removed first, leaving the child at the top).\n const wasOnTop = index === this.stack.length - 1;\n\n // Deactivate the removed focus trap so its event listeners don't interfere\n focusTrap.deactivate();\n this.stack.splice(index, 1);\n\n // activate the previous focus trap only if removed from top\n if (wasOnTop) {\n const previous = this.stack[this.stack.length - 1];\n\n if (previous) {\n previous.activate();\n }\n }\n }\n}\n\n// create a global stack of focus traps\nconst focusTrapStack = new FocusTrapStack();\n\n/**\n * The state for the FocusTrap primitive.\n */\nexport interface NgpFocusTrapState {\n // No public API\n}\n\n/**\n * The props for the FocusTrap primitive.\n */\nexport interface NgpFocusTrapProps {\n /**\n * Whether the focus trap is disabled.\n */\n readonly disabled?: Signal<boolean>;\n\n /**\n * The focus origin to use when programmatically focusing elements.\n * If not provided, falls back to the FocusMonitor's last known origin.\n */\n readonly focusOrigin?: Signal<FocusOrigin>;\n}\n\nexport const [NgpFocusTrapStateToken, ngpFocusTrap, injectFocusTrapState, provideFocusTrapState] =\n createPrimitive(\n 'NgpFocusTrap',\n ({ disabled = signal(false), focusOrigin }: NgpFocusTrapProps) => {\n const element = injectElementRef();\n const overlay = inject(NgpOverlay, { optional: true });\n const injector = inject(Injector);\n const focusMonitor = inject(FocusMonitor);\n const interactivityChecker = inject(InteractivityChecker);\n const ngZone = inject(NgZone);\n\n // Create a new focus trap\n const focusTrap = new FocusTrap();\n\n // Store the mutation observer\n let mutationObserver: MutationObserver | null = null;\n\n // Store the last focused element\n let lastFocusedElement: HTMLElement | null = null;\n\n // Host bindings\n attrBinding(element, 'tabindex', '-1');\n dataBinding(element, 'data-focus-trap', () => (disabled() ? null : ''));\n\n // Setup the focus trap\n function setupFocusTrap(): void {\n focusTrapStack.add(focusTrap);\n\n mutationObserver = new MutationObserver(handleMutations);\n\n // Setup event listeners\n ngZone.runOutsideAngular(() => {\n mutationObserver!.observe(element.nativeElement, {\n childList: true,\n subtree: true,\n });\n document.addEventListener('focusin', handleFocusIn);\n document.addEventListener('focusout', handleFocusOut);\n });\n\n const previouslyFocusedElement = document.activeElement as HTMLElement | null;\n const hasFocusedCandidate = element.nativeElement.contains(previouslyFocusedElement);\n\n // Only perform initial focusing if the focus trap is not disabled\n if (!hasFocusedCandidate && !disabled?.()) {\n // we do this to ensure the content is rendered before we try to find the first focusable element\n // and focus it\n afterNextRender(\n {\n write: () => {\n focusFirst();\n\n // if the focus didn't change, focus the container\n if (document.activeElement === previouslyFocusedElement) {\n focus(element.nativeElement);\n }\n },\n },\n { injector },\n );\n }\n }\n\n function teardownFocusTrap(): void {\n // Remove from stack (this also deactivates the trap)\n focusTrapStack.remove(focusTrap);\n mutationObserver?.disconnect();\n mutationObserver = null;\n\n // Remove event listeners\n document.removeEventListener('focusin', handleFocusIn);\n document.removeEventListener('focusout', handleFocusOut);\n }\n\n function handleFocusIn(event: FocusEvent): void {\n if (!focusTrap.active || disabled?.()) {\n return;\n }\n\n const target = event.target as HTMLElement | null;\n\n if (element.nativeElement.contains(target)) {\n lastFocusedElement = target;\n } else {\n focus(lastFocusedElement);\n }\n }\n\n /**\n * Handles the `focusout` event.\n */\n function handleFocusOut(event: FocusEvent) {\n if (!focusTrap.active || disabled?.() || event.relatedTarget === null) {\n return;\n }\n\n const relatedTarget = event.relatedTarget as HTMLElement;\n\n if (!element.nativeElement.contains(relatedTarget)) {\n focus(lastFocusedElement);\n }\n }\n\n /**\n * If the focused element gets removed from the DOM, browsers move focus back to the document.body.\n * We move focus to the container to keep focus trapped correctly.\n */\n function handleMutations(mutations: MutationRecord[]): void {\n // Don't handle mutations if the focus trap is not active (e.g., during overlay close)\n if (!focusTrap.active || disabled?.()) {\n return;\n }\n\n const focusedElement = document.activeElement as HTMLElement | null;\n\n if (focusedElement !== document.body) {\n return;\n }\n\n for (const mutation of mutations) {\n if (mutation.removedNodes.length > 0) {\n focus(element.nativeElement);\n }\n }\n }\n\n /**\n * Handles the `keydown` event.\n */\n function handleKeyDown(event: KeyboardEvent): void {\n if (!focusTrap.active || disabled?.()) {\n return;\n }\n\n const isTabKey = event.key === 'Tab' && !event.altKey && !event.ctrlKey && !event.metaKey;\n const focusedElement = document.activeElement as HTMLElement | null;\n\n if (isTabKey && focusedElement) {\n const container = event.currentTarget as HTMLElement;\n const [first, last] = getTabbableEdges(container);\n const hasTabbableElementsInside = first && last;\n\n // we can only wrap focus if we have tabbable edges\n if (!hasTabbableElementsInside) {\n if (focusedElement === container) {\n event.preventDefault();\n }\n } else {\n if (!event.shiftKey && focusedElement === last) {\n event.preventDefault();\n focus(first);\n } else if (event.shiftKey && focusedElement === first) {\n event.preventDefault();\n focus(last);\n }\n }\n }\n }\n\n /**\n * Returns the first and last tabbable elements inside a container.\n */\n function getTabbableEdges(container: HTMLElement) {\n const candidates = getTabbableCandidates(container);\n const first = findVisible(candidates);\n const last = findVisible(candidates.reverse());\n return [first, last] as const;\n }\n\n /**\n * Returns a list of potential focusable elements inside a container.\n */\n function getTabbableCandidates(container: HTMLElement) {\n const nodes: HTMLElement[] = [];\n const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {\n acceptNode: (node: HTMLElement) =>\n interactivityChecker.isFocusable(node)\n ? NodeFilter.FILTER_ACCEPT\n : NodeFilter.FILTER_SKIP,\n });\n while (walker.nextNode()) {\n nodes.push(walker.currentNode as HTMLElement);\n }\n return nodes;\n }\n\n /**\n * Returns the first visible element in a list.\n */\n function findVisible(elements: HTMLElement[]) {\n return elements.find(element => interactivityChecker.isVisible(element)) ?? null;\n }\n\n function focus(element: HTMLElement | null): void {\n if (!element) {\n return;\n }\n // Use the provided focus origin if available, otherwise fall back to FocusMonitor's last origin\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const origin = focusOrigin?.() ?? (focusMonitor as any)._lastFocusOrigin ?? 'program';\n focusMonitor.focusVia(element, origin, {\n preventScroll: true,\n });\n }\n\n function focusFirst(): void {\n const previouslyFocusedElement = document.activeElement;\n\n for (const candidate of getTabbableCandidates(element.nativeElement)) {\n focus(candidate);\n\n if (document.activeElement !== previouslyFocusedElement) {\n return;\n }\n }\n }\n\n // Setup the focus trap\n setupFocusTrap();\n\n // Teardown the focus trap on destroy\n onDestroy(teardownFocusTrap);\n\n // Listen to keydown events\n listener(element, 'keydown', handleKeyDown);\n\n // if this is used within an overlay we must remove the focus trap from the stack as soon as the overlay is closing\n // this prevents reactivation of parent focus traps during component destruction\n overlay?.closing.pipe(safeTakeUntilDestroyed()).subscribe(() => {\n focusTrapStack.remove(focusTrap);\n });\n\n return {} satisfies NgpFocusTrapState;\n },\n );\n","import { BooleanInput } from '@angular/cdk/coercion';\nimport { booleanAttribute, Directive, input } from '@angular/core';\nimport { ngpFocusTrap, provideFocusTrapState } from './focus-trap-state';\n\n/**\n * This implementation is based on the Radix UI FocusScope:\n * https://github.com/radix-ui/primitives/blob/main/packages/react/focus-scope/src/FocusScope.tsx#L306\n */\n\n/**\n * The `NgpFocusTrap` directive traps focus within the host element.\n */\n@Directive({\n selector: '[ngpFocusTrap]',\n exportAs: 'ngpFocusTrap',\n providers: [provideFocusTrapState()],\n})\nexport class NgpFocusTrap {\n /**\n * Whether the focus trap is disabled.\n */\n readonly disabled = input<boolean, BooleanInput>(false, {\n alias: 'ngpFocusTrapDisabled',\n transform: booleanAttribute,\n });\n\n /**\n * The focus trap state.\n */\n constructor() {\n ngpFocusTrap({ disabled: this.disabled });\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ng-primitives-focus-trap.mjs","sources":["../../../../packages/ng-primitives/focus-trap/src/focus-trap/focus-trap-state.ts","../../../../packages/ng-primitives/focus-trap/src/focus-trap/focus-trap.ts","../../../../packages/ng-primitives/focus-trap/src/ng-primitives-focus-trap.ts"],"sourcesContent":["import { FocusMonitor, FocusOrigin, InteractivityChecker } from '@angular/cdk/a11y';\nimport { afterNextRender, inject, Injector, NgZone, signal, Signal } from '@angular/core';\nimport { injectElementRef } from 'ng-primitives/internal';\nimport { NgpOverlay } from 'ng-primitives/portal';\nimport {\n attrBinding,\n createPrimitive,\n dataBinding,\n listener,\n onDestroy,\n} from 'ng-primitives/state';\nimport { safeTakeUntilDestroyed } from 'ng-primitives/utils';\n\nclass FocusTrap {\n /**\n * Whether the focus trap is active.\n */\n active: boolean = false;\n\n /**\n * Activates the focus trap.\n */\n activate(): void {\n this.active = true;\n }\n\n /**\n * Deactivates the focus trap.\n */\n deactivate(): void {\n this.active = false;\n }\n}\n\nclass FocusTrapStack {\n /**\n * The stack of focus traps.\n */\n private readonly stack: FocusTrap[] = [];\n\n /**\n * Adds a focus trap to the stack.\n */\n add(focusTrap: FocusTrap): void {\n // deactivate the previous focus trap\n this.stack.forEach(t => t.deactivate());\n\n // add the new focus trap and activate it\n this.stack.push(focusTrap);\n focusTrap.activate();\n }\n\n /**\n * Removes a focus trap from the stack.\n */\n remove(focusTrap: FocusTrap): void {\n // remove the focus trap\n const index = this.stack.indexOf(focusTrap);\n\n if (index < 0) {\n return;\n }\n\n // Only reactivate the previous trap if we're removing from the top of the stack.\n // This prevents reactivating a parent trap when closing all overlays (where the\n // parent is removed first, leaving the child at the top).\n const wasOnTop = index === this.stack.length - 1;\n\n // Deactivate the removed focus trap so its event listeners don't interfere\n focusTrap.deactivate();\n this.stack.splice(index, 1);\n\n // activate the previous focus trap only if removed from top\n if (wasOnTop) {\n const previous = this.stack[this.stack.length - 1];\n\n if (previous) {\n previous.activate();\n }\n }\n }\n}\n\n// create a global stack of focus traps\nconst focusTrapStack = new FocusTrapStack();\n\n/**\n * The state for the FocusTrap primitive.\n */\nexport interface NgpFocusTrapState {\n // No public API\n}\n\n/**\n * The props for the FocusTrap primitive.\n */\nexport interface NgpFocusTrapProps {\n /**\n * Whether the focus trap is disabled.\n */\n readonly disabled?: Signal<boolean>;\n\n /**\n * The focus origin to use when programmatically focusing elements.\n * If not provided, falls back to the FocusMonitor's last known origin.\n */\n readonly focusOrigin?: Signal<FocusOrigin>;\n}\n\nexport const [NgpFocusTrapStateToken, ngpFocusTrap, injectFocusTrapState, provideFocusTrapState] =\n createPrimitive(\n 'NgpFocusTrap',\n ({ disabled = signal(false), focusOrigin }: NgpFocusTrapProps) => {\n const element = injectElementRef();\n const overlay = inject(NgpOverlay, { optional: true });\n const injector = inject(Injector);\n const focusMonitor = inject(FocusMonitor);\n const interactivityChecker = inject(InteractivityChecker);\n const ngZone = inject(NgZone);\n\n // Create a new focus trap\n const focusTrap = new FocusTrap();\n\n // Store the mutation observer\n let mutationObserver: MutationObserver | null = null;\n\n // Store the last focused element\n let lastFocusedElement: HTMLElement | null = null;\n\n // Host bindings\n attrBinding(element, 'tabindex', '-1');\n dataBinding(element, 'data-focus-trap', () => (disabled() ? null : ''));\n\n // Setup the focus trap\n function setupFocusTrap(): void {\n focusTrapStack.add(focusTrap);\n\n mutationObserver = new MutationObserver(handleMutations);\n\n // Setup event listeners\n ngZone.runOutsideAngular(() => {\n mutationObserver!.observe(element.nativeElement, {\n childList: true,\n subtree: true,\n });\n document.addEventListener('focusin', handleFocusIn);\n document.addEventListener('focusout', handleFocusOut);\n });\n\n const previouslyFocusedElement = document.activeElement as HTMLElement | null;\n const hasFocusedCandidate = element.nativeElement.contains(previouslyFocusedElement);\n\n // Only perform initial focusing if the focus trap is not disabled\n if (!hasFocusedCandidate && !disabled?.()) {\n // we do this to ensure the content is rendered before we try to find the first focusable element\n // and focus it\n afterNextRender(\n {\n write: () => {\n focusFirst();\n\n // if the focus didn't change, focus the container\n if (document.activeElement === previouslyFocusedElement) {\n focus(element.nativeElement);\n }\n },\n },\n { injector },\n );\n }\n }\n\n function teardownFocusTrap(): void {\n // Remove from stack (this also deactivates the trap)\n focusTrapStack.remove(focusTrap);\n mutationObserver?.disconnect();\n mutationObserver = null;\n\n // Remove event listeners\n document.removeEventListener('focusin', handleFocusIn);\n document.removeEventListener('focusout', handleFocusOut);\n }\n\n function handleFocusIn(event: FocusEvent): void {\n if (!focusTrap.active || disabled?.()) {\n return;\n }\n\n const target = event.target as HTMLElement | null;\n\n if (element.nativeElement.contains(target)) {\n lastFocusedElement = target;\n } else if (isAllowedExternalTarget(target)) {\n // Don't interfere — see `isAllowedExternalTarget` for details.\n } else {\n focus(lastFocusedElement);\n }\n }\n\n /**\n * Handles the `focusout` event.\n */\n function handleFocusOut(event: FocusEvent) {\n if (!focusTrap.active || disabled?.() || event.relatedTarget === null) {\n return;\n }\n\n const relatedTarget = event.relatedTarget as HTMLElement;\n\n if (\n !element.nativeElement.contains(relatedTarget) &&\n !isAllowedExternalTarget(relatedTarget)\n ) {\n focus(lastFocusedElement);\n }\n }\n\n /**\n * Whether the given element belongs to another focus trap or a CDK overlay container.\n *\n * When focus moves to these elements we must not redirect it back, because they\n * manage their own focus trapping. Without this check, opening a CDK overlay\n * (e.g. a select dropdown) from inside a focus-trapped dialog would immediately\n * yank focus back into the dialog, making the overlay unusable.\n *\n * See https://github.com/nicecod3r/ng-primitives/issues/682\n * and https://github.com/nicecod3r/ng-primitives/issues/687\n */\n function isAllowedExternalTarget(target: HTMLElement | null): boolean {\n return !!(\n target?.closest?.('[data-focus-trap]') || target?.closest?.('.cdk-overlay-container')\n );\n }\n\n /**\n * If the focused element gets removed from the DOM, browsers move focus back to the document.body.\n * We move focus to the container to keep focus trapped correctly.\n */\n function handleMutations(mutations: MutationRecord[]): void {\n // Don't handle mutations if the focus trap is not active (e.g., during overlay close)\n if (!focusTrap.active || disabled?.()) {\n return;\n }\n\n const focusedElement = document.activeElement as HTMLElement | null;\n\n if (focusedElement !== document.body) {\n return;\n }\n\n for (const mutation of mutations) {\n if (mutation.removedNodes.length > 0) {\n focus(element.nativeElement);\n }\n }\n }\n\n /**\n * Handles the `keydown` event.\n */\n function handleKeyDown(event: KeyboardEvent): void {\n if (!focusTrap.active || disabled?.()) {\n return;\n }\n\n const isTabKey = event.key === 'Tab' && !event.altKey && !event.ctrlKey && !event.metaKey;\n const focusedElement = document.activeElement as HTMLElement | null;\n\n if (isTabKey && focusedElement) {\n const container = event.currentTarget as HTMLElement;\n const [first, last] = getTabbableEdges(container);\n const hasTabbableElementsInside = first && last;\n\n // we can only wrap focus if we have tabbable edges\n if (!hasTabbableElementsInside) {\n if (focusedElement === container) {\n event.preventDefault();\n }\n } else {\n if (!event.shiftKey && focusedElement === last) {\n event.preventDefault();\n focus(first);\n } else if (event.shiftKey && focusedElement === first) {\n event.preventDefault();\n focus(last);\n }\n }\n }\n }\n\n /**\n * Returns the first and last tabbable elements inside a container.\n */\n function getTabbableEdges(container: HTMLElement) {\n const candidates = getTabbableCandidates(container);\n const first = findVisible(candidates);\n const last = findVisible(candidates.reverse());\n return [first, last] as const;\n }\n\n /**\n * Returns a list of potential focusable elements inside a container.\n */\n function getTabbableCandidates(container: HTMLElement) {\n const nodes: HTMLElement[] = [];\n const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {\n acceptNode: (node: HTMLElement) =>\n interactivityChecker.isTabbable(node)\n ? NodeFilter.FILTER_ACCEPT\n : NodeFilter.FILTER_SKIP,\n });\n while (walker.nextNode()) {\n nodes.push(walker.currentNode as HTMLElement);\n }\n return nodes;\n }\n\n /**\n * Returns the first visible element in a list.\n */\n function findVisible(elements: HTMLElement[]) {\n return elements.find(element => interactivityChecker.isVisible(element)) ?? null;\n }\n\n function focus(element: HTMLElement | null): void {\n if (!element) {\n return;\n }\n // Use the provided focus origin if available, otherwise fall back to FocusMonitor's last origin\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const origin = focusOrigin?.() ?? (focusMonitor as any)._lastFocusOrigin ?? 'program';\n focusMonitor.focusVia(element, origin, {\n preventScroll: true,\n });\n }\n\n function focusFirst(): void {\n const previouslyFocusedElement = document.activeElement;\n\n for (const candidate of getTabbableCandidates(element.nativeElement)) {\n focus(candidate);\n\n if (document.activeElement !== previouslyFocusedElement) {\n return;\n }\n }\n }\n\n // Setup the focus trap\n setupFocusTrap();\n\n // Teardown the focus trap on destroy\n onDestroy(teardownFocusTrap);\n\n // Listen to keydown events\n listener(element, 'keydown', handleKeyDown);\n\n // if this is used within an overlay we must remove the focus trap from the stack as soon as the overlay is closing\n // this prevents reactivation of parent focus traps during component destruction\n overlay?.closing.pipe(safeTakeUntilDestroyed()).subscribe(() => {\n focusTrapStack.remove(focusTrap);\n });\n\n return {} satisfies NgpFocusTrapState;\n },\n );\n","import { BooleanInput } from '@angular/cdk/coercion';\nimport { booleanAttribute, Directive, input } from '@angular/core';\nimport { ngpFocusTrap, provideFocusTrapState } from './focus-trap-state';\n\n/**\n * This implementation is based on the Radix UI FocusScope:\n * https://github.com/radix-ui/primitives/blob/main/packages/react/focus-scope/src/FocusScope.tsx#L306\n */\n\n/**\n * The `NgpFocusTrap` directive traps focus within the host element.\n */\n@Directive({\n selector: '[ngpFocusTrap]',\n exportAs: 'ngpFocusTrap',\n providers: [provideFocusTrapState()],\n})\nexport class NgpFocusTrap {\n /**\n * Whether the focus trap is disabled.\n */\n readonly disabled = input<boolean, BooleanInput>(false, {\n alias: 'ngpFocusTrapDisabled',\n transform: booleanAttribute,\n });\n\n /**\n * The focus trap state.\n */\n constructor() {\n ngpFocusTrap({ disabled: this.disabled });\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createOverlay, injectOverlay, coerceOffset, coerceFlip, coerceShift } from 'ng-primitives/portal';
|
|
2
2
|
export { injectOverlayContext as injectMenuContext } from 'ng-primitives/portal';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
|
-
import { InjectionToken, inject, Injector, ViewContainerRef, signal, computed, effect, input, booleanAttribute, Directive, numberAttribute } from '@angular/core';
|
|
4
|
+
import { InjectionToken, inject, Injector, ViewContainerRef, signal, computed, effect, input, booleanAttribute, Directive, output, numberAttribute } from '@angular/core';
|
|
5
5
|
import { ngpRovingFocusItem, provideRovingFocusItemState, ngpRovingFocusGroup, provideRovingFocusGroupState } from 'ng-primitives/roving-focus';
|
|
6
6
|
import { ngpButton } from 'ng-primitives/button';
|
|
7
7
|
import { injectElementRef } from 'ng-primitives/internal';
|
|
8
|
-
import { createPrimitive, controlled, attrBinding, dataBinding, listener, deprecatedSetter, styleBinding } from 'ng-primitives/state';
|
|
8
|
+
import { createPrimitive, controlled, attrBinding, dataBinding, listener, deprecatedSetter, styleBinding, emitter } from 'ng-primitives/state';
|
|
9
9
|
import { Directionality } from '@angular/cdk/bidi';
|
|
10
10
|
import { ngpFocusTrap, provideFocusTrapState } from 'ng-primitives/focus-trap';
|
|
11
11
|
import { Subject } from 'rxjs';
|
|
@@ -694,13 +694,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
694
694
|
}]
|
|
695
695
|
}], propDecorators: { menu: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpSubmenuTrigger", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpSubmenuTriggerDisabled", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpSubmenuTriggerPlacement", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpSubmenuTriggerOffset", required: false }] }], flip: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpSubmenuTriggerFlip", required: false }] }] } });
|
|
696
696
|
|
|
697
|
-
const [NgpMenuItemStateToken, ngpMenuItem, injectMenuItemState, provideMenuItemState] = createPrimitive('NgpMenuItem', ({ disabled = signal(false) }) => {
|
|
697
|
+
const [NgpMenuItemStateToken, ngpMenuItem, injectMenuItemState, provideMenuItemState] = createPrimitive('NgpMenuItem', ({ disabled = signal(false), closeOnSelect = signal(true), role = 'menuitem', }) => {
|
|
698
698
|
const element = injectElementRef();
|
|
699
699
|
const injector = inject(Injector);
|
|
700
700
|
const parentMenu = injectMenuState({ optional: true });
|
|
701
701
|
ngpButton({ disabled });
|
|
702
702
|
// Host bindings
|
|
703
|
-
attrBinding(element, 'role',
|
|
703
|
+
attrBinding(element, 'role', role);
|
|
704
704
|
// Event listeners
|
|
705
705
|
listener(element, 'click', onClick);
|
|
706
706
|
listener(element, 'keydown', handleArrowKey);
|
|
@@ -711,7 +711,8 @@ const [NgpMenuItemStateToken, ngpMenuItem, injectMenuItemState, provideMenuItemS
|
|
|
711
711
|
const trigger = injector.get(NgpSubmenuTrigger, null, { self: true, optional: true });
|
|
712
712
|
const origin = event.detail === 0 ? 'keyboard' : 'mouse';
|
|
713
713
|
// if this is a submenu trigger, we don't want to close the menu, we want to open the submenu
|
|
714
|
-
if (
|
|
714
|
+
// if closeOnSelect is false, we don't want to close the menu (e.g., checkbox/radio items)
|
|
715
|
+
if (!trigger && closeOnSelect()) {
|
|
715
716
|
parentMenu()?.closeAllMenus(origin);
|
|
716
717
|
}
|
|
717
718
|
}
|
|
@@ -754,11 +755,17 @@ class NgpMenuItem {
|
|
|
754
755
|
alias: 'ngpMenuItemDisabled',
|
|
755
756
|
transform: booleanAttribute,
|
|
756
757
|
}]));
|
|
757
|
-
|
|
758
|
+
/** Whether the menu should close when this item is selected */
|
|
759
|
+
this.closeOnSelect = input(true, ...(ngDevMode ? [{ debugName: "closeOnSelect", alias: 'ngpMenuItemCloseOnSelect',
|
|
760
|
+
transform: booleanAttribute }] : [{
|
|
761
|
+
alias: 'ngpMenuItemCloseOnSelect',
|
|
762
|
+
transform: booleanAttribute,
|
|
763
|
+
}]));
|
|
764
|
+
ngpMenuItem({ disabled: this.disabled, closeOnSelect: this.closeOnSelect });
|
|
758
765
|
ngpRovingFocusItem({ disabled: this.disabled });
|
|
759
766
|
}
|
|
760
767
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpMenuItem, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
761
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.9", type: NgpMenuItem, isStandalone: true, selector: "[ngpMenuItem]", inputs: { disabled: { classPropertyName: "disabled", publicName: "ngpMenuItemDisabled", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideMenuItemState(), provideRovingFocusItemState()], exportAs: ["ngpMenuItem"], ngImport: i0 }); }
|
|
768
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.9", type: NgpMenuItem, isStandalone: true, selector: "[ngpMenuItem]", inputs: { disabled: { classPropertyName: "disabled", publicName: "ngpMenuItemDisabled", isSignal: true, isRequired: false, transformFunction: null }, closeOnSelect: { classPropertyName: "closeOnSelect", publicName: "ngpMenuItemCloseOnSelect", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideMenuItemState(), provideRovingFocusItemState()], exportAs: ["ngpMenuItem"], ngImport: i0 }); }
|
|
762
769
|
}
|
|
763
770
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpMenuItem, decorators: [{
|
|
764
771
|
type: Directive,
|
|
@@ -767,7 +774,200 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
767
774
|
exportAs: 'ngpMenuItem',
|
|
768
775
|
providers: [provideMenuItemState(), provideRovingFocusItemState()],
|
|
769
776
|
}]
|
|
770
|
-
}], ctorParameters: () => [], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpMenuItemDisabled", required: false }] }] } });
|
|
777
|
+
}], ctorParameters: () => [], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpMenuItemDisabled", required: false }] }], closeOnSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpMenuItemCloseOnSelect", required: false }] }] } });
|
|
778
|
+
|
|
779
|
+
const [NgpMenuItemCheckboxStateToken, ngpMenuItemCheckbox, injectMenuItemCheckboxState, provideMenuItemCheckboxState,] = createPrimitive('NgpMenuItemCheckbox', ({ checked: _checked = signal(false), disabled = signal(false), onCheckedChange, }) => {
|
|
780
|
+
const element = injectElementRef();
|
|
781
|
+
const checked = controlled(_checked);
|
|
782
|
+
const checkedChange = emitter();
|
|
783
|
+
// Use base menu item behavior but don't close on select
|
|
784
|
+
ngpMenuItem({ disabled, closeOnSelect: signal(false), role: 'menuitemcheckbox' });
|
|
785
|
+
// Host bindings
|
|
786
|
+
attrBinding(element, 'aria-checked', checked);
|
|
787
|
+
dataBinding(element, 'data-checked', checked);
|
|
788
|
+
// Toggle on click
|
|
789
|
+
listener(element, 'click', () => toggle());
|
|
790
|
+
function toggle() {
|
|
791
|
+
if (disabled()) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const nextChecked = !checked();
|
|
795
|
+
checked.set(nextChecked);
|
|
796
|
+
onCheckedChange?.(nextChecked);
|
|
797
|
+
checkedChange.emit(nextChecked);
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
checked,
|
|
801
|
+
checkedChange: checkedChange.asObservable(),
|
|
802
|
+
toggle,
|
|
803
|
+
};
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* The `NgpMenuItemCheckbox` directive represents a menu item that can be toggled on and off.
|
|
808
|
+
*/
|
|
809
|
+
class NgpMenuItemCheckbox {
|
|
810
|
+
constructor() {
|
|
811
|
+
/** Whether the checkbox is checked */
|
|
812
|
+
this.checked = input(false, ...(ngDevMode ? [{ debugName: "checked", alias: 'ngpMenuItemCheckboxChecked',
|
|
813
|
+
transform: booleanAttribute }] : [{
|
|
814
|
+
alias: 'ngpMenuItemCheckboxChecked',
|
|
815
|
+
transform: booleanAttribute,
|
|
816
|
+
}]));
|
|
817
|
+
/** Event emitted when the checked state changes */
|
|
818
|
+
this.checkedChange = output({
|
|
819
|
+
alias: 'ngpMenuItemCheckboxCheckedChange',
|
|
820
|
+
});
|
|
821
|
+
/** Whether the menu item checkbox is disabled */
|
|
822
|
+
this.disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", alias: 'ngpMenuItemCheckboxDisabled',
|
|
823
|
+
transform: booleanAttribute }] : [{
|
|
824
|
+
alias: 'ngpMenuItemCheckboxDisabled',
|
|
825
|
+
transform: booleanAttribute,
|
|
826
|
+
}]));
|
|
827
|
+
ngpMenuItemCheckbox({
|
|
828
|
+
checked: this.checked,
|
|
829
|
+
disabled: this.disabled,
|
|
830
|
+
onCheckedChange: value => this.checkedChange.emit(value),
|
|
831
|
+
});
|
|
832
|
+
ngpRovingFocusItem({ disabled: this.disabled });
|
|
833
|
+
}
|
|
834
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpMenuItemCheckbox, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
835
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.9", type: NgpMenuItemCheckbox, isStandalone: true, selector: "[ngpMenuItemCheckbox]", inputs: { checked: { classPropertyName: "checked", publicName: "ngpMenuItemCheckboxChecked", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "ngpMenuItemCheckboxDisabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checkedChange: "ngpMenuItemCheckboxCheckedChange" }, providers: [provideMenuItemCheckboxState(), provideRovingFocusItemState()], exportAs: ["ngpMenuItemCheckbox"], ngImport: i0 }); }
|
|
836
|
+
}
|
|
837
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpMenuItemCheckbox, decorators: [{
|
|
838
|
+
type: Directive,
|
|
839
|
+
args: [{
|
|
840
|
+
selector: '[ngpMenuItemCheckbox]',
|
|
841
|
+
exportAs: 'ngpMenuItemCheckbox',
|
|
842
|
+
providers: [provideMenuItemCheckboxState(), provideRovingFocusItemState()],
|
|
843
|
+
}]
|
|
844
|
+
}], ctorParameters: () => [], propDecorators: { checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpMenuItemCheckboxChecked", required: false }] }], checkedChange: [{ type: i0.Output, args: ["ngpMenuItemCheckboxCheckedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpMenuItemCheckboxDisabled", required: false }] }] } });
|
|
845
|
+
|
|
846
|
+
const [NgpMenuItemRadioGroupStateToken, ngpMenuItemRadioGroup, injectMenuItemRadioGroupState, provideMenuItemRadioGroupState,] = createPrimitive('NgpMenuItemRadioGroup', ({ value: _value = signal(null), onValueChange, }) => {
|
|
847
|
+
const element = injectElementRef();
|
|
848
|
+
const value = controlled(_value);
|
|
849
|
+
const valueChange = emitter();
|
|
850
|
+
// Host bindings
|
|
851
|
+
attrBinding(element, 'role', 'group');
|
|
852
|
+
function select(newValue) {
|
|
853
|
+
if (value() === newValue) {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
value.set(newValue);
|
|
857
|
+
onValueChange?.(newValue);
|
|
858
|
+
valueChange.emit(newValue);
|
|
859
|
+
}
|
|
860
|
+
return {
|
|
861
|
+
value,
|
|
862
|
+
valueChange: valueChange.asObservable(),
|
|
863
|
+
select,
|
|
864
|
+
};
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
const [NgpMenuItemRadioStateToken, ngpMenuItemRadio, injectMenuItemRadioState, provideMenuItemRadioState,] = createPrimitive('NgpMenuItemRadio', ({ value, disabled = signal(false) }) => {
|
|
868
|
+
const element = injectElementRef();
|
|
869
|
+
const radioGroup = injectMenuItemRadioGroupState();
|
|
870
|
+
// Use base menu item behavior but don't close on select
|
|
871
|
+
ngpMenuItem({ disabled, closeOnSelect: signal(false), role: 'menuitemradio' });
|
|
872
|
+
// Computed checked state
|
|
873
|
+
const checked = computed(() => radioGroup()?.value() === value(), ...(ngDevMode ? [{ debugName: "checked" }] : []));
|
|
874
|
+
// Host bindings
|
|
875
|
+
attrBinding(element, 'aria-checked', checked);
|
|
876
|
+
dataBinding(element, 'data-checked', checked);
|
|
877
|
+
// Select on click
|
|
878
|
+
listener(element, 'click', () => {
|
|
879
|
+
if (disabled()) {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
radioGroup()?.select(value());
|
|
883
|
+
});
|
|
884
|
+
return {
|
|
885
|
+
checked,
|
|
886
|
+
};
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* The `NgpMenuItemIndicator` directive renders inside a checkbox or radio menu item
|
|
891
|
+
* and exposes `data-checked` based on the parent item's checked state.
|
|
892
|
+
*/
|
|
893
|
+
class NgpMenuItemIndicator {
|
|
894
|
+
constructor() {
|
|
895
|
+
const element = injectElementRef();
|
|
896
|
+
const checkboxState = injectMenuItemCheckboxState({ optional: true });
|
|
897
|
+
const radioState = injectMenuItemRadioState({ optional: true });
|
|
898
|
+
const checked = computed(() => checkboxState()?.checked() ?? radioState()?.checked() ?? false, ...(ngDevMode ? [{ debugName: "checked" }] : []));
|
|
899
|
+
dataBinding(element, 'data-checked', checked);
|
|
900
|
+
}
|
|
901
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpMenuItemIndicator, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
902
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.9", type: NgpMenuItemIndicator, isStandalone: true, selector: "[ngpMenuItemIndicator]", exportAs: ["ngpMenuItemIndicator"], ngImport: i0 }); }
|
|
903
|
+
}
|
|
904
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpMenuItemIndicator, decorators: [{
|
|
905
|
+
type: Directive,
|
|
906
|
+
args: [{
|
|
907
|
+
selector: '[ngpMenuItemIndicator]',
|
|
908
|
+
exportAs: 'ngpMenuItemIndicator',
|
|
909
|
+
}]
|
|
910
|
+
}], ctorParameters: () => [] });
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* The `NgpMenuItemRadioGroup` directive represents a group of radio menu items.
|
|
914
|
+
*/
|
|
915
|
+
class NgpMenuItemRadioGroup {
|
|
916
|
+
constructor() {
|
|
917
|
+
/** The current value of the radio group */
|
|
918
|
+
this.value = input(null, ...(ngDevMode ? [{ debugName: "value", alias: 'ngpMenuItemRadioGroupValue' }] : [{
|
|
919
|
+
alias: 'ngpMenuItemRadioGroupValue',
|
|
920
|
+
}]));
|
|
921
|
+
/** Event emitted when the value changes */
|
|
922
|
+
this.valueChange = output({
|
|
923
|
+
alias: 'ngpMenuItemRadioGroupValueChange',
|
|
924
|
+
});
|
|
925
|
+
ngpMenuItemRadioGroup({
|
|
926
|
+
value: this.value,
|
|
927
|
+
onValueChange: value => this.valueChange.emit(value),
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpMenuItemRadioGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
931
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.9", type: NgpMenuItemRadioGroup, isStandalone: true, selector: "[ngpMenuItemRadioGroup]", inputs: { value: { classPropertyName: "value", publicName: "ngpMenuItemRadioGroupValue", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "ngpMenuItemRadioGroupValueChange" }, providers: [provideMenuItemRadioGroupState()], exportAs: ["ngpMenuItemRadioGroup"], ngImport: i0 }); }
|
|
932
|
+
}
|
|
933
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpMenuItemRadioGroup, decorators: [{
|
|
934
|
+
type: Directive,
|
|
935
|
+
args: [{
|
|
936
|
+
selector: '[ngpMenuItemRadioGroup]',
|
|
937
|
+
exportAs: 'ngpMenuItemRadioGroup',
|
|
938
|
+
providers: [provideMenuItemRadioGroupState()],
|
|
939
|
+
}]
|
|
940
|
+
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpMenuItemRadioGroupValue", required: false }] }], valueChange: [{ type: i0.Output, args: ["ngpMenuItemRadioGroupValueChange"] }] } });
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* The `NgpMenuItemRadio` directive represents a radio menu item within a radio group.
|
|
944
|
+
*/
|
|
945
|
+
class NgpMenuItemRadio {
|
|
946
|
+
constructor() {
|
|
947
|
+
/** The value this radio item represents */
|
|
948
|
+
this.value = input.required(...(ngDevMode ? [{ debugName: "value", alias: 'ngpMenuItemRadioValue' }] : [{
|
|
949
|
+
alias: 'ngpMenuItemRadioValue',
|
|
950
|
+
}]));
|
|
951
|
+
/** Whether the radio item is disabled */
|
|
952
|
+
this.disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", alias: 'ngpMenuItemRadioDisabled',
|
|
953
|
+
transform: booleanAttribute }] : [{
|
|
954
|
+
alias: 'ngpMenuItemRadioDisabled',
|
|
955
|
+
transform: booleanAttribute,
|
|
956
|
+
}]));
|
|
957
|
+
ngpMenuItemRadio({ value: this.value, disabled: this.disabled });
|
|
958
|
+
ngpRovingFocusItem({ disabled: this.disabled });
|
|
959
|
+
}
|
|
960
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpMenuItemRadio, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
961
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.9", type: NgpMenuItemRadio, isStandalone: true, selector: "[ngpMenuItemRadio]", inputs: { value: { classPropertyName: "value", publicName: "ngpMenuItemRadioValue", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "ngpMenuItemRadioDisabled", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideMenuItemRadioState(), provideRovingFocusItemState()], exportAs: ["ngpMenuItemRadio"], ngImport: i0 }); }
|
|
962
|
+
}
|
|
963
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NgpMenuItemRadio, decorators: [{
|
|
964
|
+
type: Directive,
|
|
965
|
+
args: [{
|
|
966
|
+
selector: '[ngpMenuItemRadio]',
|
|
967
|
+
exportAs: 'ngpMenuItemRadio',
|
|
968
|
+
providers: [provideMenuItemRadioState(), provideRovingFocusItemState()],
|
|
969
|
+
}]
|
|
970
|
+
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpMenuItemRadioValue", required: true }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngpMenuItemRadioDisabled", required: false }] }] } });
|
|
771
971
|
|
|
772
972
|
/**
|
|
773
973
|
* The `NgpMenuTrigger` directive allows you to turn an element into a menu trigger.
|
|
@@ -986,5 +1186,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
|
|
|
986
1186
|
* Generated bundle index. Do not edit.
|
|
987
1187
|
*/
|
|
988
1188
|
|
|
989
|
-
export { NgpMenu, NgpMenuItem, NgpMenuTrigger, NgpSubmenuTrigger, injectMenuItemState, injectMenuState, injectMenuTriggerState, injectSubmenuTriggerState, provideMenuConfig, provideMenuItemState, provideMenuState, provideMenuTriggerState, provideSubmenuTriggerState };
|
|
1189
|
+
export { NgpMenu, NgpMenuItem, NgpMenuItemCheckbox, NgpMenuItemIndicator, NgpMenuItemRadio, NgpMenuItemRadioGroup, NgpMenuTrigger, NgpSubmenuTrigger, injectMenuItemCheckboxState, injectMenuItemRadioGroupState, injectMenuItemRadioState, injectMenuItemState, injectMenuState, injectMenuTriggerState, injectSubmenuTriggerState, provideMenuConfig, provideMenuItemCheckboxState, provideMenuItemRadioGroupState, provideMenuItemRadioState, provideMenuItemState, provideMenuState, provideMenuTriggerState, provideSubmenuTriggerState };
|
|
990
1190
|
//# sourceMappingURL=ng-primitives-menu.mjs.map
|