@eagami/ui 5.17.1 → 5.18.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,44 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, makeEnvironmentProviders, provideEnvironmentInitializer, signal, inject, effect, computed, Injectable, input, ChangeDetectionStrategy, Component, HostBinding, Directive, model, output, Injector, afterNextRender, DestroyRef, viewChild, ElementRef, HostListener, forwardRef, Renderer2, ViewContainerRef, TemplateRef, untracked, ViewEncapsulation, contentChild, viewChildren, PLATFORM_ID } from '@angular/core';
2
+ import { InjectionToken, makeEnvironmentProviders, provideEnvironmentInitializer, inject, CSP_NONCE, signal, effect, computed, Injectable, input, ChangeDetectionStrategy, Component, HostBinding, Directive, model, output, Injector, afterNextRender, DestroyRef, viewChild, ElementRef, HostListener, forwardRef, Renderer2, ViewContainerRef, TemplateRef, untracked, ViewEncapsulation, contentChild, viewChildren, PLATFORM_ID } from '@angular/core';
3
3
  import { NgClass, NgComponentOutlet, NgTemplateOutlet, isPlatformBrowser } from '@angular/common';
4
4
  import { NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';
5
5
 
6
- /**
7
- * Display name, flag, and reading direction for every built-in locale, in
8
- * language-switcher order: English first (the default fallback), then by each
9
- * language's own name (Latin scripts first, then Greek, Cyrillic, Hebrew,
10
- * Arabic, Devanagari, and Chinese by Unicode block order). Keyed by
11
- * `EagamiLocale` so a locale added to the union without an entry fails to
12
- * compile, keeping this single source exhaustive.
13
- */
14
- const LOCALE_DISPLAY = {
15
- en: { label: 'English', flag: '🇬🇧', dir: 'ltr' },
16
- de: { label: 'Deutsch', flag: '🇩🇪', dir: 'ltr' },
17
- 'es-ES': { label: 'Español', flag: '🇪🇸', dir: 'ltr' },
18
- 'fr-FR': { label: 'Français', flag: '🇫🇷', dir: 'ltr' },
19
- is: { label: 'Íslenska', flag: '🇮🇸', dir: 'ltr' },
20
- nl: { label: 'Nederlands', flag: '🇳🇱', dir: 'ltr' },
21
- pl: { label: 'Polski', flag: '🇵🇱', dir: 'ltr' },
22
- 'pt-BR': { label: 'Português (Brasil)', flag: '🇧🇷', dir: 'ltr' },
23
- el: { label: 'Ελληνικά', flag: '🇬🇷', dir: 'ltr' },
24
- ru: { label: 'Русский', flag: '🇷🇺', dir: 'ltr' },
25
- uk: { label: 'Українська', flag: '🇺🇦', dir: 'ltr' },
26
- he: { label: 'עברית', flag: '🇮🇱', dir: 'rtl' },
27
- ar: { label: 'العربية', flag: '🇸🇦', dir: 'rtl' },
28
- hi: { label: 'हिन्दी', flag: '🇮🇳', dir: 'ltr' },
29
- 'zh-CN': { label: '中文', flag: '🇨🇳', dir: 'ltr' },
30
- };
31
- /**
32
- * Display metadata for every built-in locale. The single source the Storybook
33
- * locale toolbar and any consumer-built language switcher derive from, so the
34
- * displayed list never drifts from the locales the library ships.
35
- */
36
- const EAGAMI_LOCALE_META = Object.keys(LOCALE_DISPLAY).map(locale => ({ locale, ...LOCALE_DISPLAY[locale] }));
37
- /**
38
- * Supported locales for language switchers, in display order. Derived from
39
- * `EAGAMI_LOCALE_META` so the two never drift.
40
- */
41
- const EAGAMI_LOCALES = EAGAMI_LOCALE_META.map(m => m.locale);
6
+ /** DI token carrying the consumer-supplied i18n configuration. */
7
+ const EAGAMI_I18N_CONFIG = new InjectionToken('EAGAMI_I18N_CONFIG');
42
8
 
43
9
  const STYLE_TAG_ID = 'eagami-palette';
44
10
  /**
@@ -51,8 +17,12 @@ const STYLE_TAG_ID = 'eagami-palette';
51
17
  * Idempotent: subsequent calls replace the previous stylesheet rather than
52
18
  * append, so a theme switcher can re-apply without leaking style tags.
53
19
  * No-ops outside a DOM context (SSR).
20
+ *
21
+ * Pass `nonce` under a `style-src` policy that requires one, or the browser
22
+ * drops the stylesheet and the app silently renders the library's own colours.
23
+ * `provideEagamiUi()` forwards Angular's `CSP_NONCE` automatically.
54
24
  */
55
- function applyPalette(palette) {
25
+ function applyPalette(palette, nonce) {
56
26
  if (typeof document === 'undefined') {
57
27
  return;
58
28
  }
@@ -61,6 +31,10 @@ function applyPalette(palette) {
61
31
  if (!tag) {
62
32
  tag = document.createElement('style');
63
33
  tag.id = STYLE_TAG_ID;
34
+ // Before insertion: the policy is checked as the tag enters the document
35
+ if (nonce) {
36
+ tag.setAttribute('nonce', nonce);
37
+ }
64
38
  document.head.appendChild(tag);
65
39
  }
66
40
  tag.textContent = css;
@@ -347,8 +321,6 @@ function formatViolations(violations) {
347
321
  ].join('\n');
348
322
  }
349
323
 
350
- /** DI token carrying the consumer-supplied i18n configuration. */
351
- const EAGAMI_I18N_CONFIG = new InjectionToken('EAGAMI_I18N_CONFIG');
352
324
  /**
353
325
  * Configures Eagami UI for the application.
354
326
  *
@@ -378,6 +350,10 @@ const EAGAMI_I18N_CONFIG = new InjectionToken('EAGAMI_I18N_CONFIG');
378
350
  * },
379
351
  * });
380
352
  * ```
353
+ *
354
+ * A `palette` installs a `<style>` tag at bootstrap, carrying Angular's
355
+ * `CSP_NONCE` when the app provides one, so a strict `style-src` policy admits
356
+ * it instead of dropping the theme.
381
357
  */
382
358
  function provideEagamiUi(config = {}) {
383
359
  const i18nConfig = {
@@ -397,11 +373,48 @@ function provideEagamiUi(config = {}) {
397
373
  if (violations.length > 0) {
398
374
  throw new Error(formatViolations(violations));
399
375
  }
400
- applyPalette(palette);
376
+ applyPalette(palette, inject(CSP_NONCE, { optional: true }));
401
377
  }),
402
378
  ]);
403
379
  }
404
380
 
381
+ /**
382
+ * Display name, flag, and reading direction for every built-in locale, in
383
+ * language-switcher order: English first (the default fallback), then by each
384
+ * language's own name (Latin scripts first, then Greek, Cyrillic, Hebrew,
385
+ * Arabic, Devanagari, and Chinese by Unicode block order). Keyed by
386
+ * `EagamiLocale` so a locale added to the union without an entry fails to
387
+ * compile, keeping this single source exhaustive.
388
+ */
389
+ const LOCALE_DISPLAY = {
390
+ en: { label: 'English', flag: '🇬🇧', dir: 'ltr' },
391
+ de: { label: 'Deutsch', flag: '🇩🇪', dir: 'ltr' },
392
+ 'es-ES': { label: 'Español', flag: '🇪🇸', dir: 'ltr' },
393
+ 'fr-FR': { label: 'Français', flag: '🇫🇷', dir: 'ltr' },
394
+ is: { label: 'Íslenska', flag: '🇮🇸', dir: 'ltr' },
395
+ nl: { label: 'Nederlands', flag: '🇳🇱', dir: 'ltr' },
396
+ pl: { label: 'Polski', flag: '🇵🇱', dir: 'ltr' },
397
+ 'pt-BR': { label: 'Português (Brasil)', flag: '🇧🇷', dir: 'ltr' },
398
+ el: { label: 'Ελληνικά', flag: '🇬🇷', dir: 'ltr' },
399
+ ru: { label: 'Русский', flag: '🇷🇺', dir: 'ltr' },
400
+ uk: { label: 'Українська', flag: '🇺🇦', dir: 'ltr' },
401
+ he: { label: 'עברית', flag: '🇮🇱', dir: 'rtl' },
402
+ ar: { label: 'العربية', flag: '🇸🇦', dir: 'rtl' },
403
+ hi: { label: 'हिन्दी', flag: '🇮🇳', dir: 'ltr' },
404
+ 'zh-CN': { label: '中文', flag: '🇨🇳', dir: 'ltr' },
405
+ };
406
+ /**
407
+ * Display metadata for every built-in locale. The single source the Storybook
408
+ * locale toolbar and any consumer-built language switcher derive from, so the
409
+ * displayed list never drifts from the locales the library ships.
410
+ */
411
+ const EAGAMI_LOCALE_META = Object.keys(LOCALE_DISPLAY).map(locale => ({ locale, ...LOCALE_DISPLAY[locale] }));
412
+ /**
413
+ * Supported locales for language switchers, in display order. Derived from
414
+ * `EAGAMI_LOCALE_META` so the two never drift.
415
+ */
416
+ const EAGAMI_LOCALES = EAGAMI_LOCALE_META.map(m => m.locale);
417
+
405
418
  /**
406
419
  * Module-level signal that lets external tooling (notably the Storybook
407
420
  * preview decorator) imperatively override the active locale on a running
@@ -4328,6 +4341,73 @@ function isRtl(element) {
4328
4341
  return getComputedStyle(element).direction === 'rtl';
4329
4342
  }
4330
4343
 
4344
+ /*
4345
+ * A modal `<dialog>` (`showModal()`) renders in the browser's top layer, which
4346
+ * paints above every z-index in the normal layer. An overlay portaled to
4347
+ * `<body>` therefore disappears behind the modal it was opened from, no matter
4348
+ * how high `--z-index-popover` climbs. The popover API is the only way to join
4349
+ * that layer; `manual` opts out of the UA's light-dismiss and Escape handling
4350
+ * so the owning component keeps full control of dismissal. Top-layer elements
4351
+ * stack in promotion order, so an overlay shown while a modal is open always
4352
+ * lands above it.
4353
+ *
4354
+ * Promotion is deliberately conditional: outside a top-layer container the
4355
+ * existing z-index scale already orders overlays against each other (toasts
4356
+ * above popovers, tooltips above both), and promoting unconditionally would
4357
+ * flatten that scale into "whatever opened last wins".
4358
+ */
4359
+ const TOP_LAYER_CONTAINER = 'dialog:modal, :popover-open';
4360
+ // `selector()` takes one complex selector, so probing a comma-separated list
4361
+ // parses as invalid and answers false everywhere; test each selector alone
4362
+ let selectorSupport = null;
4363
+ function popoverSupported() {
4364
+ if (typeof HTMLElement === 'undefined' ||
4365
+ typeof HTMLElement.prototype.showPopover !== 'function' ||
4366
+ typeof CSS === 'undefined') {
4367
+ return false;
4368
+ }
4369
+ selectorSupport ??=
4370
+ CSS.supports?.('selector(dialog:modal)') === true &&
4371
+ CSS.supports?.('selector(:popover-open)') === true;
4372
+ return selectorSupport;
4373
+ }
4374
+ function isPromoted(el) {
4375
+ return el.hasAttribute('popover') && el.matches(':popover-open');
4376
+ }
4377
+ /**
4378
+ * Raises `surface` into the top layer when `anchor` sits inside something
4379
+ * that is already there. Callers must do this before measuring the surface: a
4380
+ * `popover` element is `display: none` until shown, so any rect read while it
4381
+ * is still hidden comes back zeroed.
4382
+ */
4383
+ function enterTopLayer(surface, anchor) {
4384
+ if (!popoverSupported() || !surface.isConnected || isPromoted(surface)) {
4385
+ return;
4386
+ }
4387
+ if (!anchor.closest(TOP_LAYER_CONTAINER)) {
4388
+ return;
4389
+ }
4390
+ surface.setAttribute('popover', 'manual');
4391
+ try {
4392
+ surface.showPopover();
4393
+ }
4394
+ catch {
4395
+ // Refused promotion would leave the surface stuck at the UA's
4396
+ // `display: none`, so drop back to plain z-index stacking
4397
+ surface.removeAttribute('popover');
4398
+ }
4399
+ }
4400
+ /** Returns `surface` to the normal layer, undoing {@link enterTopLayer}. */
4401
+ function leaveTopLayer(surface) {
4402
+ if (!surface.hasAttribute('popover')) {
4403
+ return;
4404
+ }
4405
+ if (surface.isConnected && isPromoted(surface)) {
4406
+ surface.hidePopover();
4407
+ }
4408
+ surface.removeAttribute('popover');
4409
+ }
4410
+
4331
4411
  /** True for cardinal placements that centre the popover on the perpendicular axis. */
4332
4412
  function isCardinal(placement) {
4333
4413
  return (placement === 'top' ||
@@ -4620,10 +4700,18 @@ class PopoverComponent {
4620
4700
  const anchor = this.resolveAnchor();
4621
4701
  const isOpen = this.open();
4622
4702
  if (!surface || !anchor || !isOpen) {
4703
+ if (surface) {
4704
+ leaveTopLayer(surface);
4705
+ }
4623
4706
  this.position.set(null);
4624
4707
  this.stable.set(false);
4625
4708
  return;
4626
4709
  }
4710
+ // Join the top layer before the first measurement below, so a popover
4711
+ // opened from inside a modal is not painted behind it. Promoting first
4712
+ // also gives the surface layout: a `popover` element is `display: none`
4713
+ // until shown, and would measure as a zero-sized box.
4714
+ enterTopLayer(surface, anchor);
4627
4715
  // Re-read inputs so signal subscriptions stay current after a re-open
4628
4716
  this.placement();
4629
4717
  this.offset();
@@ -4820,14 +4908,17 @@ class PopoverComponent {
4820
4908
  first.focus();
4821
4909
  }
4822
4910
  }
4823
- onEscape() {
4911
+ onEscape(event) {
4824
4912
  if (!this.open() || !this.closeOnEscape()) {
4825
4913
  return;
4826
4914
  }
4915
+ // Consume the key, or a native modal hosting this popover reads the same
4916
+ // Escape as its own close request and both shut at once
4917
+ event.preventDefault();
4827
4918
  this.closeRequested.emit();
4828
4919
  }
4829
4920
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PopoverComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4830
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.0.8", type: PopoverComponent, isStandalone: true, selector: "ea-popover", inputs: { anchor: { classPropertyName: "anchor", publicName: "anchor", isSignal: true, isRequired: true, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, trapFocus: { classPropertyName: "trapFocus", publicName: "trapFocus", isSignal: true, isRequired: false, transformFunction: null }, surfaceId: { classPropertyName: "surfaceId", publicName: "surfaceId", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, flip: { classPropertyName: "flip", publicName: "flip", isSignal: true, isRequired: false, transformFunction: null }, clamp: { classPropertyName: "clamp", publicName: "clamp", isSignal: true, isRequired: false, transformFunction: null }, matchAnchorWidth: { classPropertyName: "matchAnchorWidth", publicName: "matchAnchorWidth", isSignal: true, isRequired: false, transformFunction: null }, closeOnOutsideClick: { classPropertyName: "closeOnOutsideClick", publicName: "closeOnOutsideClick", isSignal: true, isRequired: false, transformFunction: null }, closeOnEscape: { classPropertyName: "closeOnEscape", publicName: "closeOnEscape", isSignal: true, isRequired: false, transformFunction: null }, scrollBehavior: { classPropertyName: "scrollBehavior", publicName: "scrollBehavior", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closeRequested: "closeRequested" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()" }, properties: { "attr.role": "null", "attr.aria-label": "null", "attr.aria-labelledby": "null" } }, viewQueries: [{ propertyName: "surfaceEl", first: true, predicate: ["surfaceEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n The surface is rendered unconditionally so the `<ng-content/>` slot always\n exists. If we gated it on `@if (open())`, Angular would re-project the\n consumer's content at the popover host's position whenever the surface was\n absent, leaking menu items / picker controls into the document flow (made\n worse by `display: contents` on the host). Hiding via `display: none` keeps\n the projected DOM owned by the surface and out of the flow when closed.\n-->\n<div\n #surfaceEl\n [class]=\"surfaceClass()\"\n [id]=\"surfaceId()\"\n [attr.role]=\"open() ? role() : null\"\n [attr.aria-label]=\"open() ? ariaLabel() : null\"\n [attr.aria-labelledby]=\"open() ? ariaLabelledby() : null\"\n [attr.aria-hidden]=\"open() ? null : true\"\n [style]=\"surfaceStyle()\"\n (keydown)=\"onSurfaceKeydown($event)\">\n <ng-content />\n</div>\n", styles: [":host{display:contents}.ea-popover__surface{z-index:var(--z-index-popover);position:fixed;visibility:hidden;font-family:var(--font-family-sans)}.ea-popover__surface--positioned{visibility:visible}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4921
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.0.8", type: PopoverComponent, isStandalone: true, selector: "ea-popover", inputs: { anchor: { classPropertyName: "anchor", publicName: "anchor", isSignal: true, isRequired: true, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, trapFocus: { classPropertyName: "trapFocus", publicName: "trapFocus", isSignal: true, isRequired: false, transformFunction: null }, surfaceId: { classPropertyName: "surfaceId", publicName: "surfaceId", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, flip: { classPropertyName: "flip", publicName: "flip", isSignal: true, isRequired: false, transformFunction: null }, clamp: { classPropertyName: "clamp", publicName: "clamp", isSignal: true, isRequired: false, transformFunction: null }, matchAnchorWidth: { classPropertyName: "matchAnchorWidth", publicName: "matchAnchorWidth", isSignal: true, isRequired: false, transformFunction: null }, closeOnOutsideClick: { classPropertyName: "closeOnOutsideClick", publicName: "closeOnOutsideClick", isSignal: true, isRequired: false, transformFunction: null }, closeOnEscape: { classPropertyName: "closeOnEscape", publicName: "closeOnEscape", isSignal: true, isRequired: false, transformFunction: null }, scrollBehavior: { classPropertyName: "scrollBehavior", publicName: "scrollBehavior", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closeRequested: "closeRequested" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape($event)" }, properties: { "attr.role": "null", "attr.aria-label": "null", "attr.aria-labelledby": "null" } }, viewQueries: [{ propertyName: "surfaceEl", first: true, predicate: ["surfaceEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n The surface is rendered unconditionally so the `<ng-content/>` slot always\n exists. If we gated it on `@if (open())`, Angular would re-project the\n consumer's content at the popover host's position whenever the surface was\n absent, leaking menu items / picker controls into the document flow (made\n worse by `display: contents` on the host). Hiding via `display: none` keeps\n the projected DOM owned by the surface and out of the flow when closed.\n-->\n<div\n #surfaceEl\n [class]=\"surfaceClass()\"\n [id]=\"surfaceId()\"\n [attr.role]=\"open() ? role() : null\"\n [attr.aria-label]=\"open() ? ariaLabel() : null\"\n [attr.aria-labelledby]=\"open() ? ariaLabelledby() : null\"\n [attr.aria-hidden]=\"open() ? null : true\"\n [style]=\"surfaceStyle()\"\n (keydown)=\"onSurfaceKeydown($event)\">\n <ng-content />\n</div>\n", styles: [":host{display:contents}.ea-popover__surface{z-index:var(--z-index-popover);position:fixed;visibility:hidden;font-family:var(--font-family-sans)}.ea-popover__surface--positioned{visibility:visible}.ea-popover__surface[popover]{inset:auto;overflow:visible;width:auto;height:auto;padding:0;margin:0;border:0;background:none;color:inherit}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4831
4922
  }
4832
4923
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PopoverComponent, decorators: [{
4833
4924
  type: Component,
@@ -4835,13 +4926,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
4835
4926
  '[attr.role]': 'null',
4836
4927
  '[attr.aria-label]': 'null',
4837
4928
  '[attr.aria-labelledby]': 'null',
4838
- }, template: "<!--\n The surface is rendered unconditionally so the `<ng-content/>` slot always\n exists. If we gated it on `@if (open())`, Angular would re-project the\n consumer's content at the popover host's position whenever the surface was\n absent, leaking menu items / picker controls into the document flow (made\n worse by `display: contents` on the host). Hiding via `display: none` keeps\n the projected DOM owned by the surface and out of the flow when closed.\n-->\n<div\n #surfaceEl\n [class]=\"surfaceClass()\"\n [id]=\"surfaceId()\"\n [attr.role]=\"open() ? role() : null\"\n [attr.aria-label]=\"open() ? ariaLabel() : null\"\n [attr.aria-labelledby]=\"open() ? ariaLabelledby() : null\"\n [attr.aria-hidden]=\"open() ? null : true\"\n [style]=\"surfaceStyle()\"\n (keydown)=\"onSurfaceKeydown($event)\">\n <ng-content />\n</div>\n", styles: [":host{display:contents}.ea-popover__surface{z-index:var(--z-index-popover);position:fixed;visibility:hidden;font-family:var(--font-family-sans)}.ea-popover__surface--positioned{visibility:visible}\n"] }]
4929
+ }, template: "<!--\n The surface is rendered unconditionally so the `<ng-content/>` slot always\n exists. If we gated it on `@if (open())`, Angular would re-project the\n consumer's content at the popover host's position whenever the surface was\n absent, leaking menu items / picker controls into the document flow (made\n worse by `display: contents` on the host). Hiding via `display: none` keeps\n the projected DOM owned by the surface and out of the flow when closed.\n-->\n<div\n #surfaceEl\n [class]=\"surfaceClass()\"\n [id]=\"surfaceId()\"\n [attr.role]=\"open() ? role() : null\"\n [attr.aria-label]=\"open() ? ariaLabel() : null\"\n [attr.aria-labelledby]=\"open() ? ariaLabelledby() : null\"\n [attr.aria-hidden]=\"open() ? null : true\"\n [style]=\"surfaceStyle()\"\n (keydown)=\"onSurfaceKeydown($event)\">\n <ng-content />\n</div>\n", styles: [":host{display:contents}.ea-popover__surface{z-index:var(--z-index-popover);position:fixed;visibility:hidden;font-family:var(--font-family-sans)}.ea-popover__surface--positioned{visibility:visible}.ea-popover__surface[popover]{inset:auto;overflow:visible;width:auto;height:auto;padding:0;margin:0;border:0;background:none;color:inherit}\n"] }]
4839
4930
  }], ctorParameters: () => [], propDecorators: { surfaceEl: [{ type: i0.ViewChild, args: ['surfaceEl', { isSignal: true }] }], anchor: [{ type: i0.Input, args: [{ isSignal: true, alias: "anchor", required: true }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], role: [{ type: i0.Input, args: [{ isSignal: true, alias: "role", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-labelledby", required: false }] }], trapFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "trapFocus", required: false }] }], surfaceId: [{ type: i0.Input, args: [{ isSignal: true, alias: "surfaceId", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], flip: [{ type: i0.Input, args: [{ isSignal: true, alias: "flip", required: false }] }], clamp: [{ type: i0.Input, args: [{ isSignal: true, alias: "clamp", required: false }] }], matchAnchorWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "matchAnchorWidth", required: false }] }], closeOnOutsideClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnOutsideClick", required: false }] }], closeOnEscape: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnEscape", required: false }] }], scrollBehavior: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollBehavior", required: false }] }], closeRequested: [{ type: i0.Output, args: ["closeRequested"] }], onDocumentClick: [{
4840
4931
  type: HostListener,
4841
4932
  args: ['document:click', ['$event']]
4842
4933
  }], onEscape: [{
4843
4934
  type: HostListener,
4844
- args: ['document:keydown.escape']
4935
+ args: ['document:keydown.escape', ['$event']]
4845
4936
  }] } });
4846
4937
 
4847
4938
  /**
@@ -5662,6 +5753,10 @@ class TooltipDirective {
5662
5753
  this.renderer.setStyle(this.tooltipEl, 'white-space', 'normal');
5663
5754
  }
5664
5755
  this.renderer.appendChild(document.body, this.tooltipEl);
5756
+ // Before any measuring below: a trigger inside a modal needs its bubble in
5757
+ // the top layer to be visible at all, and a promoted bubble only has layout
5758
+ // once shown.
5759
+ enterTopLayer(this.tooltipEl, this.el.nativeElement);
5665
5760
  document.addEventListener('keydown', this.keydownHandler);
5666
5761
  this.appendDescribedBy();
5667
5762
  this.shrinkToContent();
@@ -5700,6 +5795,7 @@ class TooltipDirective {
5700
5795
  }
5701
5796
  if (this.tooltipEl) {
5702
5797
  document.removeEventListener('keydown', this.keydownHandler);
5798
+ leaveTopLayer(this.tooltipEl);
5703
5799
  this.tooltipEl.remove();
5704
5800
  this.tooltipEl = null;
5705
5801
  this.templateView?.destroy();
@@ -5805,6 +5901,12 @@ class TooltipDirective {
5805
5901
  if (underBubble && !this.el.nativeElement.contains(underBubble)) {
5806
5902
  let cursor = underBubble;
5807
5903
  while (cursor && cursor !== document.body) {
5904
+ /* A fixed / sticky container that also holds the trigger (a modal
5905
+ dialog, a popover surface) is the surface the bubble sits on, not
5906
+ chrome covering it, so stop before mistaking it for an overlay. */
5907
+ if (cursor.contains(this.el.nativeElement)) {
5908
+ break;
5909
+ }
5808
5910
  const pos = getComputedStyle(cursor).position;
5809
5911
  if (pos === 'fixed' || pos === 'sticky') {
5810
5912
  this.hide();
@@ -5814,8 +5916,8 @@ class TooltipDirective {
5814
5916
  }
5815
5917
  }
5816
5918
  }
5817
- this.renderer.setStyle(this.tooltipEl, 'top', `${top + window.scrollY}px`);
5818
- this.renderer.setStyle(this.tooltipEl, 'left', `${left + window.scrollX}px`);
5919
+ this.renderer.setStyle(this.tooltipEl, 'top', `${top}px`);
5920
+ this.renderer.setStyle(this.tooltipEl, 'left', `${left}px`);
5819
5921
  }
5820
5922
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: TooltipDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
5821
5923
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.8", type: TooltipDirective, isStandalone: true, selector: "[eaTooltip]", inputs: { eaTooltip: { classPropertyName: "eaTooltip", publicName: "eaTooltip", isSignal: true, isRequired: true, transformFunction: null }, tooltipPosition: { classPropertyName: "tooltipPosition", publicName: "tooltipPosition", isSignal: true, isRequired: false, transformFunction: null }, maxWidth: { classPropertyName: "maxWidth", publicName: "maxWidth", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
@@ -9922,8 +10024,14 @@ class DrawerComponent {
9922
10024
  this.open.set(false);
9923
10025
  this.closed.emit();
9924
10026
  }
9925
- // Non-modal push drawers do not emit `cancel`, so Escape is handled here
10027
+ // Non-modal push drawers do not emit `cancel`, so Escape is handled here.
10028
+ // An overlay inside the drawer (menu, dropdown, picker) consumes the key it
10029
+ // acts on, which a modal drawer honours through `cancel`; respect it here too
10030
+ // so one press never dismisses both the overlay and the drawer.
9926
10031
  handleKeydown(event) {
10032
+ if (event.defaultPrevented) {
10033
+ return;
10034
+ }
9927
10035
  if (this.mode() === 'push' && this.closeOnEscape() && event.key === 'Escape') {
9928
10036
  this.handleClose();
9929
10037
  }
@@ -12501,14 +12609,17 @@ class MenuComponent {
12501
12609
  this.focusItem(items[next]);
12502
12610
  }
12503
12611
  }
12504
- onEscape() {
12612
+ onEscape(event) {
12505
12613
  if (!this.open()) {
12506
12614
  return;
12507
12615
  }
12616
+ // Consume the key, or a native modal hosting this menu reads the same
12617
+ // Escape as its own close request and both shut at once
12618
+ event.preventDefault();
12508
12619
  this.close(true);
12509
12620
  }
12510
12621
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: MenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
12511
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.0.8", type: MenuComponent, isStandalone: true, selector: "ea-menu", inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", opened: "opened", closed: "closed" }, host: { listeners: { "document:keydown": "onKeydown($event)", "document:keydown.escape": "onEscape()" } }, viewQueries: [{ propertyName: "listEl", first: true, predicate: ["listEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<ea-popover\n [anchor]=\"triggerEl()\"\n [open]=\"open()\"\n [placement]=\"placement()\"\n role=\"menu\"\n [surfaceId]=\"id()\"\n [aria-label]=\"resolvedAriaLabel()\"\n [closeOnEscape]=\"false\"\n scrollBehavior=\"reposition\"\n (closeRequested)=\"onPopoverCloseRequested()\">\n <div\n #listEl\n class=\"ea-menu__list ea-menu__list--{{ size() }}\"\n [style.max-height]=\"maxHeight()\">\n <ng-content />\n </div>\n</ea-popover>\n", styles: [":host{display:contents}.ea-menu__list{min-width:10em;padding:.25em 0;overflow-y:auto;border:var(--border-width-thin) solid var(--color-border-default);border-radius:var(--radius-md);box-shadow:var(--shadow-lg);background-color:var(--ea-menu-list-background-color, var(--color-bg-elevated))}@media(forced-colors:active){.ea-menu__list{border:1px solid CanvasText}}.ea-menu__list--xs{font-size:var(--font-size-xs)}.ea-menu__list--sm{font-size:var(--font-size-sm)}.ea-menu__list--md{font-size:var(--font-size-md)}.ea-menu__list--lg{font-size:var(--font-size-lg)}.ea-menu__list--xl{font-size:var(--font-size-xl)}\n"], dependencies: [{ kind: "component", type: PopoverComponent, selector: "ea-popover", inputs: ["anchor", "open", "placement", "role", "aria-label", "aria-labelledby", "trapFocus", "surfaceId", "offset", "flip", "clamp", "matchAnchorWidth", "closeOnOutsideClick", "closeOnEscape", "scrollBehavior"], outputs: ["closeRequested"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
12622
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.0.8", type: MenuComponent, isStandalone: true, selector: "ea-menu", inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", opened: "opened", closed: "closed" }, host: { listeners: { "document:keydown": "onKeydown($event)", "document:keydown.escape": "onEscape($event)" } }, viewQueries: [{ propertyName: "listEl", first: true, predicate: ["listEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<ea-popover\n [anchor]=\"triggerEl()\"\n [open]=\"open()\"\n [placement]=\"placement()\"\n role=\"menu\"\n [surfaceId]=\"id()\"\n [aria-label]=\"resolvedAriaLabel()\"\n [closeOnEscape]=\"false\"\n scrollBehavior=\"reposition\"\n (closeRequested)=\"onPopoverCloseRequested()\">\n <div\n #listEl\n class=\"ea-menu__list ea-menu__list--{{ size() }}\"\n [style.max-height]=\"maxHeight()\">\n <ng-content />\n </div>\n</ea-popover>\n", styles: [":host{display:contents}.ea-menu__list{min-width:10em;padding:.25em 0;overflow-y:auto;border:var(--border-width-thin) solid var(--color-border-default);border-radius:var(--radius-md);box-shadow:var(--shadow-lg);background-color:var(--ea-menu-list-background-color, var(--color-bg-elevated))}@media(forced-colors:active){.ea-menu__list{border:1px solid CanvasText}}.ea-menu__list--xs{font-size:var(--font-size-xs)}.ea-menu__list--sm{font-size:var(--font-size-sm)}.ea-menu__list--md{font-size:var(--font-size-md)}.ea-menu__list--lg{font-size:var(--font-size-lg)}.ea-menu__list--xl{font-size:var(--font-size-xl)}\n"], dependencies: [{ kind: "component", type: PopoverComponent, selector: "ea-popover", inputs: ["anchor", "open", "placement", "role", "aria-label", "aria-labelledby", "trapFocus", "surfaceId", "offset", "flip", "clamp", "matchAnchorWidth", "closeOnOutsideClick", "closeOnEscape", "scrollBehavior"], outputs: ["closeRequested"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
12512
12623
  }
12513
12624
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: MenuComponent, decorators: [{
12514
12625
  type: Component,
@@ -12518,7 +12629,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
12518
12629
  args: ['document:keydown', ['$event']]
12519
12630
  }], onEscape: [{
12520
12631
  type: HostListener,
12521
- args: ['document:keydown.escape']
12632
+ args: ['document:keydown.escape', ['$event']]
12522
12633
  }] } });
12523
12634
 
12524
12635
  /**