@uni-design-system/uni-angular 8.2.0 → 8.3.1

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,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, computed, linkedSignal, resource, Injectable, inject, DestroyRef, InjectionToken, input, ChangeDetectionStrategy, Component, contentChildren, output, Renderer2, ElementRef, Directive, model, effect, viewChild, afterNextRender, ViewChild, afterRenderEffect, booleanAttribute, viewChildren } from '@angular/core';
2
+ import { signal, computed, linkedSignal, resource, Injectable, inject, DestroyRef, InjectionToken, input, ChangeDetectionStrategy, Component, contentChildren, output, Renderer2, ElementRef, Directive, model, viewChild, effect, untracked, afterNextRender, ViewChild, afterRenderEffect, booleanAttribute, viewChildren } from '@angular/core';
3
3
  import { injectGlobal, css, keyframes } from '@emotion/css';
4
- import { UniThemes, LightTheme, toTypefaces, parseTheme, formatThemeIssues, hydrateTheme, createThemeFromPalette, Z_INDEX, fadeIn, removeInputPlatformStyling, fadeOut, EXPAND_DEFAULT_SPEED, expandDuration, expandFadeIn, collapseFadeOut, HOVER_OR_KEYBOARD_FOCUS, ShapeRadii, generatePalette, emitThemeFile, emitDtcgTokens } from '@uni-design-system/uni-core';
4
+ import { UniThemes, LightTheme, toTypefaces, parseTheme, formatThemeIssues, hydrateTheme, createThemeFromPalette, Z_INDEX, removeInputPlatformStyling, fadeIn, fadeOut, EXPAND_DEFAULT_SPEED, expandDuration, expandFadeIn, collapseFadeOut, HOVER_OR_KEYBOARD_FOCUS, ShapeRadii, generatePalette, emitThemeFile, emitDtcgTokens } from '@uni-design-system/uni-core';
5
5
  import { NgClass, NgTemplateOutlet, CommonModule } from '@angular/common';
6
6
 
7
7
  let nextUniqueId = 0;
@@ -361,6 +361,7 @@ class ListboxNavigation {
361
361
  _open = signal(false, ...(ngDevMode ? [{ debugName: "_open" }] : /* istanbul ignore next */ []));
362
362
  _activeIndex = signal(-1, ...(ngDevMode ? [{ debugName: "_activeIndex" }] : /* istanbul ignore next */ []));
363
363
  wrap;
364
+ isDisabled;
364
365
  /** Whether the popup is showing. Also false when there is nothing to show. */
365
366
  open = computed(() => this._open() && this.config.count() > 0, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
366
367
  /** Index of the highlighted option, or -1 when none is active. */
@@ -378,6 +379,7 @@ class ListboxNavigation {
378
379
  this.config = config;
379
380
  this.listboxId = uniqueId(config.idPrefix ?? 'uni-listbox');
380
381
  this.wrap = config.wrap ?? true;
382
+ this.isDisabled = config.disabled ?? (() => false);
381
383
  }
382
384
  /** Stable per-option id, for `role="option"` elements. */
383
385
  optionId(index) {
@@ -418,20 +420,35 @@ class ListboxNavigation {
418
420
  return this.step(current, 1, count);
419
421
  case 'ArrowUp':
420
422
  // Opening with ArrowUp lands on the last option, matching menus.
421
- return current < 0 ? count - 1 : this.step(current, -1, count);
423
+ return current < 0 ? this.seek(count - 1, -1, count) : this.step(current, -1, count);
422
424
  case 'Home':
423
- return 0;
425
+ return this.seek(0, 1, count);
424
426
  case 'End':
425
- return count - 1;
427
+ return this.seek(count - 1, -1, count);
426
428
  default:
427
429
  return null;
428
430
  }
429
431
  }
432
+ /** Nearest enabled index at or after `start`, walking by `delta` with wrap;
433
+ null when every option is disabled. */
434
+ seek(start, delta, count) {
435
+ for (let i = start, n = 0; n < count; n++, i += delta) {
436
+ const index = (i + count) % count;
437
+ if (!this.isDisabled(index))
438
+ return index;
439
+ }
440
+ return null;
441
+ }
430
442
  step(current, delta, count) {
431
- const next = current + delta;
432
443
  if (this.wrap)
433
- return (next + count) % count;
434
- return Math.min(Math.max(next, 0), count - 1);
444
+ return this.seek((current + delta + count) % count, delta, count);
445
+ // Clamped: walk toward the boundary only; no enabled candidate → hold position.
446
+ const from = Math.min(Math.max(current + delta, 0), count - 1);
447
+ for (let i = from; i >= 0 && i < count; i += delta) {
448
+ if (!this.isDisabled(i))
449
+ return i;
450
+ }
451
+ return current >= 0 && !this.isDisabled(current) ? current : null;
435
452
  }
436
453
  /** Close when focus leaves the control entirely (not on internal moves). */
437
454
  closeOnFocusOut(event) {
@@ -499,29 +516,59 @@ function anchorStyles(anchor, placement, offset = {}) {
499
516
  }
500
517
  return styles;
501
518
  }
519
+ /**
520
+ * The half of the arrow square kept by `clip-path`, in the square's local
521
+ * (pre-rotation) coordinates: the outer triangle, cut exactly on the
522
+ * panel-edge diagonal. Without the clip, the two bordered edges run the full
523
+ * square and their strokes visibly cut into the panel surface. The diagonal
524
+ * cut ends the strokes precisely at the panel edge, and the panel's own
525
+ * border line — which sits just *outside* that edge — falls inside the kept
526
+ * half, so the arrow background covers it and the base reads as an opening.
527
+ * Keyed by the side of the anchor the panel sits on.
528
+ *
529
+ * A positive `overlap` extends the kept half past the diagonal INTO the
530
+ * panel — only for borderless arrows that want anti-aliasing insurance along
531
+ * the base; on a bordered arrow it turns the stroke ends into visible stubs
532
+ * inside the panel body.
533
+ */
534
+ const ARROW_CLIP = {
535
+ // Arrow at the panel's top edge, borders top + left → keep the top-left half.
536
+ bottom: (d) => `polygon(0 0, ${past(d)} 0, 0 ${past(d)})`,
537
+ // Arrow at the panel's bottom edge, borders right + bottom.
538
+ top: (d) => `polygon(${before(d)} 100%, 100% ${before(d)}, 100% 100%)`,
539
+ // Arrow at the panel's right edge, borders right + top.
540
+ left: (d) => `polygon(${before(d)} 0, 100% 0, 100% ${past(d)})`,
541
+ // Arrow at the panel's left edge, borders left + bottom.
542
+ right: (d) => `polygon(0 ${before(d)}, ${past(d)} 100%, 0 100%)`,
543
+ };
544
+ const past = (d) => (d ? `calc(100% + ${d}px)` : '100%');
545
+ const before = (d) => (d ? `calc(0% - ${d}px)` : '0');
502
546
  /**
503
547
  * Absolute positioning for a rotated-square arrow sitting on the panel edge
504
548
  * that faces the anchor. Static per placement: if the browser flips the panel
505
549
  * via position-try, the arrow stays on the configured side (cosmetic-only
506
550
  * degradation, matching the flip-support caveat above).
551
+ *
552
+ * `overlap` (default 0 — cut exactly on the panel edge) extends the kept half
553
+ * into the panel; see {@link ARROW_CLIP} for when that is ever wanted.
507
554
  */
508
- function anchorArrowStyles(placement, size = 8) {
555
+ function anchorArrowStyles(placement, size = 8, overlap = 0) {
509
556
  const [side, align] = placement.split('-');
510
- const overlap = -(size / 2);
511
557
  const styles = {
512
558
  position: 'absolute',
513
559
  width: size,
514
560
  height: size,
515
561
  transform: 'rotate(45deg)',
562
+ clipPath: ARROW_CLIP[side](overlap),
516
563
  };
517
- // Edge facing the anchor
564
+ // Edge facing the anchor: half the square pokes out past the panel edge
518
565
  const facing = {
519
566
  top: 'bottom',
520
567
  bottom: 'top',
521
568
  left: 'right',
522
569
  right: 'left',
523
570
  };
524
- styles[facing[side]] = overlap;
571
+ styles[facing[side]] = -(size / 2);
525
572
  // Cross-axis position along that edge
526
573
  const horizontal = side === 'top' || side === 'bottom';
527
574
  const crossProp = horizontal ? 'left' : 'top';
@@ -537,6 +584,181 @@ function anchorArrowStyles(placement, size = 8) {
537
584
  }
538
585
  return styles;
539
586
  }
587
+ /**
588
+ * Scrim pieces that cut a spotlight hole around the element carrying
589
+ * `anchor-name: <anchor>`. Every inset is `calc(anchor(<side>) ± px)`, so the
590
+ * browser tracks the target through scroll/resize/layout with zero listeners.
591
+ *
592
+ * The window paints everything: its `border` is the focus ring and its huge
593
+ * spread shadow is the scrim (an outer shadow of a rounded rect covers the
594
+ * whole viewport except the rect, rounded corners included). It is
595
+ * click-through; the four strips do the click-blocking, and the optional
596
+ * cover blocks the hole itself. Callers append these to a fixed, transparent,
597
+ * `pointer-events: none` full-viewport container (each piece is
598
+ * `position: fixed`).
599
+ */
600
+ function spotlightStyles(anchor, options = {}) {
601
+ const pad = options.pad ?? 6;
602
+ const ringWidth = options.ringWidth ?? 2;
603
+ const scrimColor = options.scrimColor ?? 'rgba(0, 0, 0, 0.45)';
604
+ const holeInset = (extra) => Object.fromEntries(['top', 'left', 'right', 'bottom'].map((side) => [
605
+ side,
606
+ `calc(anchor(${side}) - ${pad + extra}px)`,
607
+ ]));
608
+ const piece = { position: 'fixed', positionAnchor: anchor };
609
+ const blocker = { ...piece, pointerEvents: 'auto', background: 'transparent' };
610
+ return {
611
+ window: {
612
+ ...piece,
613
+ ...holeInset(ringWidth), // the ring sits outside the padded hole
614
+ pointerEvents: 'none',
615
+ borderWidth: ringWidth,
616
+ borderStyle: 'solid',
617
+ boxShadow: `0 0 0 200vmax ${scrimColor}`,
618
+ },
619
+ strips: {
620
+ top: { ...blocker, top: 0, left: 0, right: 0, bottom: `calc(anchor(top) + ${pad}px)` },
621
+ bottom: { ...blocker, bottom: 0, left: 0, right: 0, top: `calc(anchor(bottom) + ${pad}px)` },
622
+ left: {
623
+ ...blocker,
624
+ left: 0,
625
+ right: `calc(anchor(left) + ${pad}px)`,
626
+ top: `calc(anchor(top) - ${pad}px)`,
627
+ bottom: `calc(anchor(bottom) - ${pad}px)`,
628
+ },
629
+ right: {
630
+ ...blocker,
631
+ right: 0,
632
+ left: `calc(anchor(right) + ${pad}px)`,
633
+ top: `calc(anchor(top) - ${pad}px)`,
634
+ bottom: `calc(anchor(bottom) - ${pad}px)`,
635
+ },
636
+ },
637
+ cover: { ...blocker, ...holeInset(0) },
638
+ };
639
+ }
640
+ /**
641
+ * Transform origin for a panel's open/close scale animation, derived from
642
+ * where the panel **actually** rendered relative to its anchor — not from the
643
+ * requested placement. `position-try-fallbacks` lets the browser flip a panel
644
+ * at viewport edges, and a statically mapped origin then animates from the
645
+ * wrong corner (a `bottom-end` picker flipped above its field would still
646
+ * scale from `top right`). Measure after the popover is shown and apply the
647
+ * result as an inline style.
648
+ *
649
+ * Returns keyword pairs like `'top right'` / `'bottom center'`, or `null`
650
+ * when the panel has no box yet (e.g. `display: none`, or jsdom).
651
+ */
652
+ function transformOriginFor(panel, trigger) {
653
+ if (!panel.width && !panel.height)
654
+ return null;
655
+ // Which side of the trigger the panel sits on wins; when it overlaps on an
656
+ // axis (aligned placements), the closer-aligned edge is the anchored one.
657
+ const axis = (panelStart, panelEnd, triggerStart, triggerEnd) => {
658
+ if (panelStart >= triggerEnd)
659
+ return 'start'; // panel after the trigger: grows away from its start edge
660
+ if (panelEnd <= triggerStart)
661
+ return 'end'; // panel before the trigger: grows toward its end edge
662
+ const startGap = Math.abs(panelStart - triggerStart);
663
+ const endGap = Math.abs(panelEnd - triggerEnd);
664
+ if (Math.abs(startGap - endGap) <= 1)
665
+ return 'center';
666
+ return startGap < endGap ? 'start' : 'end';
667
+ };
668
+ const y = axis(panel.top, panel.bottom, trigger.top, trigger.bottom);
669
+ const x = axis(panel.left, panel.right, trigger.left, trigger.right);
670
+ const vertical = y === 'start' ? 'top' : y === 'end' ? 'bottom' : 'center';
671
+ const horizontal = x === 'start' ? 'left' : x === 'end' ? 'right' : 'center';
672
+ return `${vertical} ${horizontal}`;
673
+ }
674
+
675
+ /**
676
+ * Shared plumbing for top-layer overlays built on the native `popover`
677
+ * attribute (dropdown, popover, callout). Pure functions and data — no DOM
678
+ * ownership — so each component keeps its own template while the anchor
679
+ * bookkeeping, discrete-transition block, and focus-restore rule stay single-
680
+ * sourced.
681
+ */
682
+ /** Placement → `transform-origin`, so scale animations grow from the anchor. */
683
+ const TRANSFORM_ORIGINS = {
684
+ top: 'bottom center',
685
+ right: 'center left',
686
+ bottom: 'top center',
687
+ left: 'center right',
688
+ 'top-start': 'bottom left',
689
+ 'top-end': 'bottom right',
690
+ 'right-start': 'top left',
691
+ 'right-end': 'bottom left',
692
+ 'bottom-start': 'top left',
693
+ 'bottom-end': 'top right',
694
+ 'left-start': 'top right',
695
+ 'left-end': 'bottom right',
696
+ };
697
+ /** Write a CSS `anchor-name` onto an element (detached anchors and targets). */
698
+ function setAnchorName(el, name) {
699
+ el.style.setProperty('anchor-name', name);
700
+ }
701
+ function clearAnchorName(el) {
702
+ el.style.removeProperty('anchor-name');
703
+ }
704
+ /**
705
+ * Resolve an element-or-id reference. Id strings are looked up at call time —
706
+ * never cache the result across opens, the element may have been re-rendered.
707
+ * `''` and null-ish mean "unset" (falsy guard: an empty string is the only
708
+ * typeable empty for a string-typed input).
709
+ */
710
+ function resolveElement(ref) {
711
+ if (!ref)
712
+ return null;
713
+ return typeof ref === 'string' ? document.getElementById(ref) : ref;
714
+ }
715
+ /** True when a native `toggle` event reports the overlay opening. */
716
+ function isToggleOpen(event) {
717
+ return event.newState === 'open';
718
+ }
719
+ /**
720
+ * The discrete-transition block that animates an element into and out of the
721
+ * top layer: `transition-behavior: allow-discrete` over the `hidden` keys plus
722
+ * `display`/`overlay`, the shown state under `:popover-open`, and
723
+ * `@starting-style` so entry transitions run from the hidden state.
724
+ */
725
+ function discreteOverlayTransition(durationMs, hidden, shown) {
726
+ return {
727
+ transitionProperty: [...Object.keys(hidden), 'display', 'overlay']
728
+ .map((key) => key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`))
729
+ .join(', '),
730
+ transitionDuration: `${durationMs}ms`,
731
+ transitionBehavior: 'allow-discrete',
732
+ ...hidden,
733
+ '&:popover-open': shown,
734
+ '@starting-style': {
735
+ '&:popover-open': hidden,
736
+ },
737
+ };
738
+ }
739
+ /**
740
+ * Returns focus to `target` when an overlay closes while focus was inside its
741
+ * panel (or was dropped on `<body>` by the top layer closing), so keyboard
742
+ * users are never stranded (WCAG 2.4.3). Focus resting anywhere else is left
743
+ * alone.
744
+ */
745
+ function restoreOverlayFocus(panel, target) {
746
+ const active = document.activeElement;
747
+ if (active === document.body || (active && panel.contains(active))) {
748
+ target?.focus();
749
+ }
750
+ }
751
+ /**
752
+ * The focusable elements inside `root`, in DOM order. Filters disabled
753
+ * controls, and hidden ones where the environment can tell: jsdom reports
754
+ * `offsetParent` as null for every element, so the visibility filter applies
755
+ * only when the document actually lays out (some element has an offsetParent).
756
+ */
757
+ function focusableElements(root) {
758
+ const all = Array.from(root.querySelectorAll(FOCUSABLE_SELECTOR)).filter((el) => !el.disabled);
759
+ const laidOut = all.some((el) => el.offsetParent !== null);
760
+ return laidOut ? all.filter((el) => el.offsetParent !== null) : all;
761
+ }
540
762
 
541
763
  class UniBaseDatasource {
542
764
  selections = signal([], ...(ngDevMode ? [{ debugName: "selections" }] : /* istanbul ignore next */ []));
@@ -3079,6 +3301,389 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3079
3301
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-calendar, Calendar', imports: [UniIconButtonComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'calendar' }], host: { '[class]': 'className()', '(focusout)': 'onHostFocusOut($event)' }, template: "<div [class]=\"navClass()\">\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n [symbolName]=\"componentOptions().navPrevSymbol ?? 'chevron_left'\"\n [disable]=\"disabled()\"\n (click)=\"onNav(-1)\"\n >\n Previous month\n </button>\n <!-- aria-live so PageUp/Down narrates the new month without refocusing. -->\n <div [class]=\"headingClass()\" [id]=\"headingId\" aria-live=\"polite\">{{ heading() }}</div>\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n [symbolName]=\"componentOptions().navNextSymbol ?? 'chevron_right'\"\n [disable]=\"disabled()\"\n (click)=\"onNav(1)\"\n >\n Next month\n </button>\n</div>\n\n<!-- One tab stop: the roving tabindex lives on the day buttons, and the grid\n itself only relays their bubbling keys (same delegation as tag-input). -->\n<!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n<div\n role=\"grid\"\n [class]=\"gridClass()\"\n [attr.aria-label]=\"ariaLabel() ?? null\"\n [attr.aria-labelledby]=\"ariaLabel() ? null : headingId\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onGridKeydown($event)\"\n>\n <div role=\"row\" [class]=\"rowClass()\">\n @for (weekday of weekdays(); track weekday.full) {\n <div role=\"columnheader\" [class]=\"weekdayClass()\">\n <abbr [attr.title]=\"weekday.full\">{{ weekday.label }}</abbr>\n </div>\n }\n </div>\n\n @for (week of gridWeeks(); track week[0].date) {\n <div role=\"row\" [class]=\"rowClass()\">\n @for (cell of week; track cell.date) {\n @if (cell.outside) {\n <!-- Outside days: hidden placeholders by default (geometry kept),\n muted and non-interactive under the showOutsideDays option. -->\n <div role=\"gridcell\" aria-hidden=\"true\" [class]=\"cell.cellClass\">\n @if (showOutside()) {\n <span [class]=\"outsideDayClass()\">{{ cell.day }}</span>\n }\n </div>\n } @else {\n <div\n role=\"gridcell\"\n [class]=\"cell.cellClass\"\n [attr.aria-selected]=\"cell.selected || cell.inBand ? true : null\"\n >\n <button\n type=\"button\"\n [class]=\"cell.dayClass\"\n [attr.data-date]=\"cell.date\"\n [tabindex]=\"cell.tabIndex\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n [disabled]=\"cell.disabled || disabled()\"\n (click)=\"select(cell.date)\"\n (mouseenter)=\"onDayHover(cell.date)\"\n >\n {{ cell.day }}\n @if (cell.markers.length) {\n <span [class]=\"dotsClass()\">\n @for (marker of cell.markers; track $index) {\n <span data-dot [class]=\"dotClassFor(marker.variant)\"></span>\n }\n </span>\n }\n </button>\n </div>\n }\n }\n </div>\n }\n</div>\n\n<!-- Selections and range progress are otherwise silent for a screen reader. -->\n<span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n" }]
3080
3302
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], month: [{ type: i0.Input, args: [{ isSignal: true, alias: "month", required: false }] }, { type: i0.Output, args: ["monthChange"] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], disabledDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledDates", required: false }] }], markers: [{ type: i0.Input, args: [{ isSignal: true, alias: "markers", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], weekStart: [{ type: i0.Input, args: [{ isSignal: true, alias: "weekStart", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], selected: [{ type: i0.Output, args: ["selected"] }] } });
3081
3303
 
3304
+ /**
3305
+ * Anchored coach-mark panel that dims the page and cuts a spotlight hole
3306
+ * around its target. Both the scrim and the panel are `popover="manual"`
3307
+ * elements shown in order, so they stack deterministically in the top layer
3308
+ * above every app z-index, and every scrim piece is CSS-anchor-positioned to
3309
+ * the target — the hole tracks scroll/resize/layout with zero listeners.
3310
+ *
3311
+ * The panel is a non-modal `role="dialog"` (deliberately no `aria-modal`):
3312
+ * focus moves into it on open, Tab runs a "duet loop" over the panel's
3313
+ * focusables plus the spotlit target while it stays interactive, and on close
3314
+ * focus returns to the pre-open element — unless the user moved into the
3315
+ * target, where it stays.
3316
+ *
3317
+ * Storage-free by rule: `key` and the `dismissed` output are the "don't show
3318
+ * again" hooks; persistence is the app's business (e.g. cdk local-storage).
3319
+ */
3320
+ class UniCalloutComponent extends BaseComponent {
3321
+ destroyRef = inject(DestroyRef);
3322
+ open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
3323
+ /** Identifies this callout in `dismissed` payloads. Empty string = unset. */
3324
+ key = input(...(ngDevMode ? [undefined, { debugName: "key" }] : /* istanbul ignore next */ []));
3325
+ /** Element (or id, resolved at open time) to spotlight. '' = unset. */
3326
+ target = input(...(ngDevMode ? [undefined, { debugName: "target" }] : /* istanbul ignore next */ []));
3327
+ placement = input('bottom', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
3328
+ /** Defaults to `spotlight` when a target resolves, else `dim`. */
3329
+ backdrop = input(...(ngDevMode ? [undefined, { debugName: "backdrop" }] : /* istanbul ignore next */ []));
3330
+ /** When false, a transparent cover blocks the spotlit target. */
3331
+ targetInteractive = input(true, ...(ngDevMode ? [{ debugName: "targetInteractive" }] : /* istanbul ignore next */ []));
3332
+ /** Gates Escape and the close button. */
3333
+ dismissible = input(true, ...(ngDevMode ? [{ debugName: "dismissible" }] : /* istanbul ignore next */ []));
3334
+ /** When true, scrim clicks close (reason `backdrop`); else the panel pulses. */
3335
+ dismissOnBackdrop = input(false, ...(ngDevMode ? [{ debugName: "dismissOnBackdrop" }] : /* istanbul ignore next */ []));
3336
+ header = input(...(ngDevMode ? [undefined, { debugName: "header" }] : /* istanbul ignore next */ []));
3337
+ arrow = input(true, ...(ngDevMode ? [{ debugName: "arrow" }] : /* istanbul ignore next */ []));
3338
+ /** Wins over the header/body labelling; the tour sets "{title}, step n of N". */
3339
+ ariaLabel = input(...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
3340
+ /** Accessible name of the dismiss button (the tour passes its skip label). */
3341
+ closeLabel = input('Close', ...(ngDevMode ? [{ debugName: "closeLabel" }] : /* istanbul ignore next */ []));
3342
+ opened = output();
3343
+ closed = output();
3344
+ dismissed = output();
3345
+ /** Keys pressed while focus is inside the panel (the tour's arrow-key hook). */
3346
+ panelKeydown = output();
3347
+ panelId = uniqueId('uni-callout');
3348
+ headerId = uniqueId('uni-callout-header');
3349
+ bodyId = uniqueId('uni-callout-body');
3350
+ anchorName = newAnchorName();
3351
+ showing = signal(false, ...(ngDevMode ? [{ debugName: "showing" }] : /* istanbul ignore next */ []));
3352
+ activeTarget = signal(null, ...(ngDevMode ? [{ debugName: "activeTarget" }] : /* istanbul ignore next */ []));
3353
+ scrimShown = signal(false, ...(ngDevMode ? [{ debugName: "scrimShown" }] : /* istanbul ignore next */ []));
3354
+ /**
3355
+ * Keeps the spotlight pieces rendered and the panel anchored through the
3356
+ * close fade — tearing the anchor down at close time made the panel jump
3357
+ * and the scrim vanish mid-transition.
3358
+ */
3359
+ scrimContent = signal(false, ...(ngDevMode ? [{ debugName: "scrimContent" }] : /* istanbul ignore next */ []));
3360
+ pendingTeardown = null;
3361
+ prevFocus = null;
3362
+ detachKeydown = null;
3363
+ stripSides = ['top', 'bottom', 'left', 'right'];
3364
+ scrimRef = viewChild.required('scrim');
3365
+ panelRef = viewChild.required('panel');
3366
+ actionsRef = viewChild.required('actions');
3367
+ constructor() {
3368
+ super();
3369
+ effect(() => {
3370
+ const want = this.open();
3371
+ if (want === untracked(this.showing))
3372
+ return;
3373
+ if (want)
3374
+ this.show();
3375
+ else
3376
+ this.close('programmatic');
3377
+ });
3378
+ // Re-seat an open callout when its target or backdrop changes (a tour
3379
+ // stepping) instead of closing and reopening — the panel stays up, the
3380
+ // original pre-open focus is preserved for the eventual close, and the
3381
+ // duet loop re-seats onto the new step's content.
3382
+ effect(() => {
3383
+ const ref = this.target();
3384
+ this.backdrop();
3385
+ if (!untracked(this.showing))
3386
+ return;
3387
+ untracked(() => this.retarget(resolveElement(ref)));
3388
+ });
3389
+ this.destroyRef.onDestroy(() => {
3390
+ this.detachKeydown?.();
3391
+ if (this.pendingTeardown)
3392
+ clearTimeout(this.pendingTeardown);
3393
+ const target = untracked(this.activeTarget);
3394
+ if (target)
3395
+ clearAnchorName(target);
3396
+ for (const ref of [this.panelRef, this.scrimRef]) {
3397
+ try {
3398
+ ref().nativeElement.hidePopover();
3399
+ }
3400
+ catch {
3401
+ // popover was already closed or detached
3402
+ }
3403
+ }
3404
+ });
3405
+ }
3406
+ effectiveBackdrop = computed(() => this.backdrop() ?? (this.activeTarget() ? 'spotlight' : 'dim'), ...(ngDevMode ? [{ debugName: "effectiveBackdrop" }] : /* istanbul ignore next */ []));
3407
+ show() {
3408
+ if (this.showing())
3409
+ return;
3410
+ if (this.pendingTeardown) {
3411
+ clearTimeout(this.pendingTeardown);
3412
+ this.pendingTeardown = null;
3413
+ }
3414
+ const target = resolveElement(this.target());
3415
+ const stale = this.activeTarget();
3416
+ if (stale && stale !== target)
3417
+ clearAnchorName(stale); // a close was mid-fade
3418
+ this.activeTarget.set(target);
3419
+ this.scrimContent.set(true);
3420
+ this.prevFocus = document.activeElement;
3421
+ // Anchor insets need the target on screen before the scrim paints; the
3422
+ // hole then tracks any further movement natively.
3423
+ target?.scrollIntoView?.({ block: 'center', behavior: 'instant' });
3424
+ if (target)
3425
+ setAnchorName(target, this.anchorName);
3426
+ // Scrim first, panel second: top-layer order is stacking order.
3427
+ if ((this.backdrop() ?? (target ? 'spotlight' : 'dim')) !== 'none') {
3428
+ this.scrimRef().nativeElement.showPopover();
3429
+ this.scrimShown.set(true);
3430
+ }
3431
+ this.panelRef().nativeElement.showPopover();
3432
+ const onKeydown = (event) => this.onKeydown(event);
3433
+ document.addEventListener('keydown', onKeydown, true);
3434
+ this.detachKeydown = () => {
3435
+ document.removeEventListener('keydown', onKeydown, true);
3436
+ this.detachKeydown = null;
3437
+ };
3438
+ this.showing.set(true);
3439
+ this.open.set(true);
3440
+ this.focusInitial();
3441
+ this.opened.emit();
3442
+ }
3443
+ close(reason) {
3444
+ if (!this.showing())
3445
+ return;
3446
+ this.detachKeydown?.();
3447
+ const target = this.activeTarget();
3448
+ const active = document.activeElement;
3449
+ const focusInTarget = !!target && (target === active || target.contains(active));
3450
+ for (const ref of [this.panelRef, this.scrimRef]) {
3451
+ try {
3452
+ ref().nativeElement.hidePopover();
3453
+ }
3454
+ catch {
3455
+ // scrim may not have been shown (backdrop="none")
3456
+ }
3457
+ }
3458
+ this.showing.set(false);
3459
+ this.scrimShown.set(false);
3460
+ this.open.set(false);
3461
+ // The close fade is still running: the panel must stay anchored and the
3462
+ // spotlight pieces rendered until it finishes, or everything jumps.
3463
+ this.pendingTeardown = setTimeout(() => {
3464
+ this.pendingTeardown = null;
3465
+ if (target)
3466
+ clearAnchorName(target);
3467
+ this.activeTarget.set(null);
3468
+ this.scrimContent.set(false);
3469
+ }, this.componentOptions().transitionMs);
3470
+ // The duet loop may have sent the user into the target on purpose — don't
3471
+ // yank them back out of it.
3472
+ if (!focusInTarget && this.prevFocus && document.contains(this.prevFocus)) {
3473
+ this.prevFocus.focus();
3474
+ }
3475
+ this.dismissed.emit({ key: this.key() || undefined, reason });
3476
+ this.closed.emit();
3477
+ }
3478
+ /** Move an open callout to a new target without a close/open round trip. */
3479
+ retarget(target) {
3480
+ const prev = this.activeTarget();
3481
+ if (prev && prev !== target)
3482
+ clearAnchorName(prev);
3483
+ target?.scrollIntoView?.({ block: 'center', behavior: 'instant' });
3484
+ if (target)
3485
+ setAnchorName(target, this.anchorName);
3486
+ this.activeTarget.set(target);
3487
+ const wantScrim = (this.backdrop() ?? (target ? 'spotlight' : 'dim')) !== 'none';
3488
+ if (wantScrim !== this.scrimShown()) {
3489
+ const scrim = this.scrimRef().nativeElement;
3490
+ const panel = this.panelRef().nativeElement;
3491
+ try {
3492
+ if (wantScrim) {
3493
+ // Top-layer order is stacking order: re-show the panel above the
3494
+ // late-arriving scrim.
3495
+ scrim.showPopover();
3496
+ panel.hidePopover();
3497
+ panel.showPopover();
3498
+ }
3499
+ else {
3500
+ scrim.hidePopover();
3501
+ }
3502
+ }
3503
+ catch {
3504
+ // a piece was already in the requested state
3505
+ }
3506
+ this.scrimShown.set(wantScrim);
3507
+ }
3508
+ this.focusInitial();
3509
+ }
3510
+ /** `[autofocus]` → first action → first panel focusable → the panel itself. */
3511
+ focusInitial() {
3512
+ const panel = this.panelRef().nativeElement;
3513
+ const first = panel.querySelector('[autofocus]') ??
3514
+ focusableElements(this.actionsRef().nativeElement)[0] ??
3515
+ focusableElements(panel)[0] ??
3516
+ panel;
3517
+ first.focus({ preventScroll: true });
3518
+ }
3519
+ /**
3520
+ * Capture phase so Escape works from inside the spotlit target and the tab
3521
+ * loop preempts the page. Non-Tab keys surface through `panelKeydown`.
3522
+ */
3523
+ onKeydown(event) {
3524
+ if (event.key === 'Escape') {
3525
+ if (this.dismissible()) {
3526
+ event.preventDefault();
3527
+ this.close('escape');
3528
+ }
3529
+ return;
3530
+ }
3531
+ if (event.key !== 'Tab') {
3532
+ if (this.panelRef().nativeElement.contains(document.activeElement)) {
3533
+ this.panelKeydown.emit(event);
3534
+ }
3535
+ return;
3536
+ }
3537
+ if (this.effectiveBackdrop() === 'none')
3538
+ return; // nothing is blocked — tab naturally
3539
+ // Duet loop: panel focusables plus the interactive target.
3540
+ const loop = focusableElements(this.panelRef().nativeElement);
3541
+ const target = this.activeTarget();
3542
+ if (target && this.targetInteractive())
3543
+ loop.push(target);
3544
+ if (!loop.length)
3545
+ return;
3546
+ const index = loop.indexOf(document.activeElement);
3547
+ event.preventDefault();
3548
+ const next = event.shiftKey
3549
+ ? loop[(index <= 0 ? loop.length : index) - 1]
3550
+ : loop[(index + 1) % loop.length];
3551
+ next.focus();
3552
+ }
3553
+ /** A stray click shouldn't kill onboarding: nudge the panel instead. */
3554
+ onBackdropClick() {
3555
+ if (this.dismissOnBackdrop()) {
3556
+ this.close('backdrop');
3557
+ return;
3558
+ }
3559
+ try {
3560
+ if (matchMedia('(prefers-reduced-motion: reduce)').matches)
3561
+ return;
3562
+ this.panelRef().nativeElement.animate?.([
3563
+ { translate: '0 0' },
3564
+ { translate: '0 -3px', offset: 0.3 },
3565
+ { translate: '0 2px', offset: 0.6 },
3566
+ { translate: '0 0' },
3567
+ ], { duration: 400, easing: 'ease' });
3568
+ }
3569
+ catch {
3570
+ // animation is decoration; environments without WAAPI just skip it
3571
+ }
3572
+ }
3573
+ // --- styling --------------------------------------------------------------
3574
+ spotlight = computed(() => {
3575
+ const options = this.componentOptions();
3576
+ return spotlightStyles(this.anchorName, {
3577
+ pad: options.spotlightPadding,
3578
+ ringWidth: options.ringWidth,
3579
+ scrimColor: options.scrimColor,
3580
+ });
3581
+ }, ...(ngDevMode ? [{ debugName: "spotlight" }] : /* istanbul ignore next */ []));
3582
+ scrimClassName = computed(() => {
3583
+ const options = this.componentOptions();
3584
+ return css({
3585
+ position: 'fixed',
3586
+ inset: 0,
3587
+ width: '100%',
3588
+ height: '100%',
3589
+ margin: 0,
3590
+ border: 0,
3591
+ padding: 0,
3592
+ background: 'transparent',
3593
+ overflow: 'visible',
3594
+ pointerEvents: 'none',
3595
+ '& > *': { position: 'fixed' },
3596
+ ...discreteOverlayTransition(options.transitionMs, { opacity: 0 }, { opacity: 1 }),
3597
+ });
3598
+ }, ...(ngDevMode ? [{ debugName: "scrimClassName" }] : /* istanbul ignore next */ []));
3599
+ windowClassName = computed(() => {
3600
+ const ring = this.theme.colorPalette()[this.variant()] ?? this.theme.colorPalette()['primary'];
3601
+ return css({
3602
+ ...this.spotlight().window,
3603
+ ...this.theme.radius(this.componentOptions().spotlightRadius),
3604
+ borderColor: ring,
3605
+ });
3606
+ }, ...(ngDevMode ? [{ debugName: "windowClassName" }] : /* istanbul ignore next */ []));
3607
+ stripClassName(side) {
3608
+ return css(this.spotlight().strips[side]);
3609
+ }
3610
+ coverClassName = computed(() => css(this.spotlight().cover), ...(ngDevMode ? [{ debugName: "coverClassName" }] : /* istanbul ignore next */ []));
3611
+ fullCoverClassName = computed(() => css({
3612
+ inset: 0,
3613
+ background: this.componentOptions().scrimColor,
3614
+ pointerEvents: 'auto',
3615
+ }), ...(ngDevMode ? [{ debugName: "fullCoverClassName" }] : /* istanbul ignore next */ []));
3616
+ panelClassName = computed(() => {
3617
+ const options = this.componentOptions();
3618
+ const anchored = this.activeTarget() !== null;
3619
+ return css({
3620
+ ...this.theme.colorPair(options.color),
3621
+ ...this.theme.radius(options.borderRadius),
3622
+ ...this.theme.boxShadow(options.shadow),
3623
+ ...this.theme.typeface(options.typeface),
3624
+ width: options.width,
3625
+ maxWidth: 'calc(100vw - 32px)',
3626
+ padding: 0,
3627
+ border: 0,
3628
+ overflow: 'visible',
3629
+ // A target-less panel keeps the popover UA styles (inset:0 + margin:auto)
3630
+ // and centers in the viewport; anchorStyles resets them for anchoring.
3631
+ ...(anchored
3632
+ ? anchorStyles(this.anchorName, this.placement(), {
3633
+ mainAxis: options.offset + options.spotlightPadding,
3634
+ })
3635
+ : {}),
3636
+ ...discreteOverlayTransition(options.transitionMs, { opacity: 0, translate: '0 6px' }, { opacity: 1, translate: '0 0' }),
3637
+ });
3638
+ }, ...(ngDevMode ? [{ debugName: "panelClassName" }] : /* istanbul ignore next */ []));
3639
+ headRowClassName = computed(() => {
3640
+ const options = this.componentOptions();
3641
+ return css({
3642
+ display: 'flex',
3643
+ alignItems: 'center',
3644
+ gap: 8,
3645
+ padding: options.padding,
3646
+ paddingBottom: 0,
3647
+ '&:not(:has(*))': { display: 'none' },
3648
+ });
3649
+ }, ...(ngDevMode ? [{ debugName: "headRowClassName" }] : /* istanbul ignore next */ []));
3650
+ titleClassName = computed(() => css({ ...this.theme.typeface(this.componentOptions().headerTypeface), marginRight: 'auto' }), ...(ngDevMode ? [{ debugName: "titleClassName" }] : /* istanbul ignore next */ []));
3651
+ bodyClassName = computed(() => {
3652
+ const options = this.componentOptions();
3653
+ return css({ padding: options.padding, paddingTop: 6, paddingBottom: 12, '&:empty': { display: 'none' } });
3654
+ }, ...(ngDevMode ? [{ debugName: "bodyClassName" }] : /* istanbul ignore next */ []));
3655
+ actionsRowClassName = computed(() => {
3656
+ const options = this.componentOptions();
3657
+ return css({
3658
+ display: 'flex',
3659
+ alignItems: 'center',
3660
+ gap: 8,
3661
+ padding: options.padding,
3662
+ paddingTop: 0,
3663
+ '&:not(:has(*))': { display: 'none' },
3664
+ });
3665
+ }, ...(ngDevMode ? [{ debugName: "actionsRowClassName" }] : /* istanbul ignore next */ []));
3666
+ arrowClassName = computed(() => {
3667
+ const options = this.componentOptions();
3668
+ return css({
3669
+ ...this.theme.colorPair(options.color),
3670
+ ...anchorArrowStyles(this.placement(), options.arrowSize),
3671
+ });
3672
+ }, ...(ngDevMode ? [{ debugName: "arrowClassName" }] : /* istanbul ignore next */ []));
3673
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCalloutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3674
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniCalloutComponent, isStandalone: true, selector: "uni-callout", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: false, transformFunction: null }, target: { classPropertyName: "target", publicName: "target", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, backdrop: { classPropertyName: "backdrop", publicName: "backdrop", isSignal: true, isRequired: false, transformFunction: null }, targetInteractive: { classPropertyName: "targetInteractive", publicName: "targetInteractive", isSignal: true, isRequired: false, transformFunction: null }, dismissible: { classPropertyName: "dismissible", publicName: "dismissible", isSignal: true, isRequired: false, transformFunction: null }, dismissOnBackdrop: { classPropertyName: "dismissOnBackdrop", publicName: "dismissOnBackdrop", isSignal: true, isRequired: false, transformFunction: null }, header: { classPropertyName: "header", publicName: "header", isSignal: true, isRequired: false, transformFunction: null }, arrow: { classPropertyName: "arrow", publicName: "arrow", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, closeLabel: { classPropertyName: "closeLabel", publicName: "closeLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", opened: "opened", closed: "closed", dismissed: "dismissed", panelKeydown: "panelKeydown" }, providers: [{ provide: COMPONENT_NAME, useValue: 'callout' }], viewQueries: [{ propertyName: "scrimRef", first: true, predicate: ["scrim"], descendants: true, isSignal: true }, { propertyName: "panelRef", first: true, predicate: ["panel"], descendants: true, isSignal: true }, { propertyName: "actionsRef", first: true, predicate: ["actions"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Scrim first: top-layer order is stacking order, so the panel always sits\n above it. Strips and cover are pointer-blockers only; the keyboard path\n is the duet loop on the document keydown handler. -->\n<div #scrim popover=\"manual\" [class]=\"scrimClassName()\">\n @if (scrimContent()) {\n @if (effectiveBackdrop() === 'spotlight' && activeTarget()) {\n <div [class]=\"windowClassName()\"></div>\n @for (side of stripSides; track side) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div [class]=\"stripClassName(side)\" (click)=\"onBackdropClick()\"></div>\n }\n @if (!targetInteractive()) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div [class]=\"coverClassName()\" (click)=\"onBackdropClick()\"></div>\n }\n } @else if (effectiveBackdrop() === 'dim') {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div [class]=\"fullCoverClassName()\" (click)=\"onBackdropClick()\"></div>\n }\n }\n</div>\n<div\n #panel\n popover=\"manual\"\n role=\"dialog\"\n tabindex=\"-1\"\n [id]=\"panelId\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabel() ? null : header() ? headerId : bodyId\"\n [class]=\"panelClassName()\"\n>\n <ng-content select=\"[callout-media]\"></ng-content>\n <div [class]=\"headRowClassName()\">\n <ng-content select=\"[callout-header]\"></ng-content>\n @if (header()) {\n <span [id]=\"headerId\" [class]=\"titleClassName()\">{{ header() }}</span>\n }\n @if (dismissible()) {\n <button\n icon-button\n [iconName]=\"componentOptions().closeSymbol\"\n variant=\"ghost\"\n size=\"sm\"\n (click)=\"close('close-button')\"\n >\n {{ closeLabel() }}\n </button>\n }\n </div>\n <div [id]=\"bodyId\" [class]=\"bodyClassName()\"><ng-content></ng-content></div>\n <div #actions [class]=\"actionsRowClassName()\"><ng-content select=\"[callout-actions]\"></ng-content></div>\n @if (arrow() && activeTarget()) {\n <span [class]=\"arrowClassName()\"></span>\n }\n</div>\n", dependencies: [{ kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3675
+ }
3676
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCalloutComponent, decorators: [{
3677
+ type: Component,
3678
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-callout', imports: [UniIconButtonComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'callout' }], template: "<!-- Scrim first: top-layer order is stacking order, so the panel always sits\n above it. Strips and cover are pointer-blockers only; the keyboard path\n is the duet loop on the document keydown handler. -->\n<div #scrim popover=\"manual\" [class]=\"scrimClassName()\">\n @if (scrimContent()) {\n @if (effectiveBackdrop() === 'spotlight' && activeTarget()) {\n <div [class]=\"windowClassName()\"></div>\n @for (side of stripSides; track side) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div [class]=\"stripClassName(side)\" (click)=\"onBackdropClick()\"></div>\n }\n @if (!targetInteractive()) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div [class]=\"coverClassName()\" (click)=\"onBackdropClick()\"></div>\n }\n } @else if (effectiveBackdrop() === 'dim') {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div [class]=\"fullCoverClassName()\" (click)=\"onBackdropClick()\"></div>\n }\n }\n</div>\n<div\n #panel\n popover=\"manual\"\n role=\"dialog\"\n tabindex=\"-1\"\n [id]=\"panelId\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabel() ? null : header() ? headerId : bodyId\"\n [class]=\"panelClassName()\"\n>\n <ng-content select=\"[callout-media]\"></ng-content>\n <div [class]=\"headRowClassName()\">\n <ng-content select=\"[callout-header]\"></ng-content>\n @if (header()) {\n <span [id]=\"headerId\" [class]=\"titleClassName()\">{{ header() }}</span>\n }\n @if (dismissible()) {\n <button\n icon-button\n [iconName]=\"componentOptions().closeSymbol\"\n variant=\"ghost\"\n size=\"sm\"\n (click)=\"close('close-button')\"\n >\n {{ closeLabel() }}\n </button>\n }\n </div>\n <div [id]=\"bodyId\" [class]=\"bodyClassName()\"><ng-content></ng-content></div>\n <div #actions [class]=\"actionsRowClassName()\"><ng-content select=\"[callout-actions]\"></ng-content></div>\n @if (arrow() && activeTarget()) {\n <span [class]=\"arrowClassName()\"></span>\n }\n</div>\n" }]
3679
+ }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: false }] }], target: [{ type: i0.Input, args: [{ isSignal: true, alias: "target", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], backdrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "backdrop", required: false }] }], targetInteractive: [{ type: i0.Input, args: [{ isSignal: true, alias: "targetInteractive", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], dismissOnBackdrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissOnBackdrop", required: false }] }], header: [{ type: i0.Input, args: [{ isSignal: true, alias: "header", required: false }] }], arrow: [{ type: i0.Input, args: [{ isSignal: true, alias: "arrow", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], closeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeLabel", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], dismissed: [{ type: i0.Output, args: ["dismissed"] }], panelKeydown: [{ type: i0.Output, args: ["panelKeydown"] }], scrimRef: [{ type: i0.ViewChild, args: ['scrim', { isSignal: true }] }], panelRef: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], actionsRef: [{ type: i0.ViewChild, args: ['actions', { isSignal: true }] }] } });
3680
+
3681
+ /**
3682
+ * UniCalloutComponent Barrel File
3683
+ *
3684
+ * This file exports all public-facing elements of the callout component.
3685
+ */
3686
+
3082
3687
  class UniCardContentComponent extends BaseComponent {
3083
3688
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCardContentComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3084
3689
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.12", type: UniCardContentComponent, isStandalone: true, selector: "uni-card-content", providers: [{ provide: COMPONENT_NAME, useValue: 'cardContent' }], usesInheritance: true, ngImport: i0, template: "<div [style]=\"style()\">\n <ng-content></ng-content>\n</div>\n", styles: [""], changeDetection: i0.ChangeDetectionStrategy.OnPush });
@@ -3342,6 +3947,525 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3342
3947
  args: [{ selector: 'uni-checkbox', imports: [UniTextComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'checkbox' }], changeDetection: ChangeDetectionStrategy.OnPush, template: "<label [class]=\"checkboxLabel()\">\n <input\n type=\"checkbox\"\n [class]=\"checkboxInput()\"\n [checked]=\"checked()\"\n [indeterminate]=\"indeterminate()\"\n (change)=\"handleChange($event)\"\n [disabled]=\"disabled()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n />\n <div class=\"checkbox\">\n <svg viewBox=\"0 0 20 20\" aria-hidden=\"true\">\n <rect class=\"checkbox-box\" x=\"1\" y=\"1\" width=\"18\" height=\"18\"></rect>\n <polyline class=\"checkbox-check\" points=\"4 11 8 15 16 6\"></polyline>\n <line class=\"checkbox-dash\" x1=\"5\" y1=\"10\" x2=\"15\" y2=\"10\"></line>\n </svg>\n </div>\n @if (label()) {\n <span uni-text=\"label\">{{ label() }}</span>\n }\n</label>\n" }]
3343
3948
  }], propDecorators: { checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], indeterminate: [{ type: i0.Input, args: [{ isSignal: true, alias: "indeterminate", required: false }] }, { type: i0.Output, args: ["indeterminateChange"] }] } });
3344
3949
 
3950
+ /**
3951
+ * Style block for the popup behind every `ListboxNavigation` consumer: an
3952
+ * absolutely-positioned `ul[role="listbox"]` under a `position: relative`
3953
+ * field wrapper, with the shared option chrome and active/hover highlight.
3954
+ *
3955
+ * Extracted because four components (`uni-search-input`, `uni-tag-input`,
3956
+ * `uni-time-input`, `uni-combobox`) carried hand-rolled copies, and the parts
3957
+ * that silently drift between copies all live here: the surface/elevation
3958
+ * trio, and the `activeColor` pair that themes re-point when their container
3959
+ * tokens don't contrast (see the Wellsourced overrides).
3960
+ *
3961
+ * Compose extras with the array form — `css([listboxPopupStyles(…), {…}])` —
3962
+ * so a component's own `& [role="option"]` block cascades after this one
3963
+ * instead of replacing it (an object spread would overwrite the key).
3964
+ */
3965
+ const listboxPopupStyles = (theme, options, { maxHeight = 280 } = {}) => ({
3966
+ position: 'absolute',
3967
+ top: '100%',
3968
+ left: 0,
3969
+ right: 0,
3970
+ zIndex: 20,
3971
+ margin: '4px 0 0',
3972
+ padding: 4,
3973
+ listStyle: 'none',
3974
+ maxHeight,
3975
+ overflowY: 'auto',
3976
+ ...theme.colorPair((options.listColor ?? 'primary-surface')),
3977
+ ...theme.boxShadow(options.listShadow ?? 'menu'),
3978
+ ...theme.radius(options.listBorderRadius ?? 'xs'),
3979
+ '& [role="option"]': {
3980
+ padding: '8px 12px',
3981
+ cursor: 'pointer',
3982
+ ...theme.typeface('label'),
3983
+ ...theme.radius('xxs'),
3984
+ '&.active, &:not([aria-disabled="true"]):hover': {
3985
+ ...theme.colorPair((options.activeColor ?? 'primary-container')),
3986
+ },
3987
+ },
3988
+ });
3989
+
3990
+ class UniInputBoxComponent extends BaseComponent {
3991
+ className = css({ display: 'contents' });
3992
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
3993
+ error = input(false, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
3994
+ minWidth = input('0', ...(ngDevMode ? [{ debugName: "minWidth" }] : /* istanbul ignore next */ []));
3995
+ /** Override the themed field height, e.g. `'auto'` for multi-line fields. */
3996
+ height = input(undefined, ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
3997
+ color = computed(() => this.error() ? this.componentOptions().errorColor : this.componentOptions().color, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
3998
+ border = computed(() => this.error() ? this.componentOptions().errorBorder : this.componentOptions().border, ...(ngDevMode ? [{ debugName: "border" }] : /* istanbul ignore next */ []));
3999
+ shadow = computed(() => this.error() ? this.componentOptions().errorShadow : this.componentOptions().shadow, ...(ngDevMode ? [{ debugName: "shadow" }] : /* istanbul ignore next */ []));
4000
+ inputBoxClass = computed(() => css([
4001
+ this.disabled() && {
4002
+ ...this.theme.color(this.componentOptions().disabledTextColor),
4003
+ ...this.theme.backgroundColor(this.componentOptions().disabledColor),
4004
+ cursor: 'not-allowed !important',
4005
+ },
4006
+ {
4007
+ '& input, select, textarea': {
4008
+ ...removeInputPlatformStyling,
4009
+ height: '100%',
4010
+ ...this.theme.paddingLeft(this.componentOptions().paddingLeft),
4011
+ ...this.theme.color(this.componentOptions().textColor),
4012
+ // `typeFace` is the deprecated casing; themes that still set it win
4013
+ // only when the canonical key is absent.
4014
+ ...this.theme.typeface(this.componentOptions().typeface ?? this.componentOptions().typeFace),
4015
+ },
4016
+ // Multi-line fields size themselves (rows/resize), not from the box.
4017
+ '& textarea': {
4018
+ height: 'auto',
4019
+ ...this.theme.paddingTop('xs'),
4020
+ ...this.theme.paddingBottom('xs'),
4021
+ },
4022
+ '&:has(input:disabled, select:disabled, textarea:disabled)': {
4023
+ ...this.theme.color(this.componentOptions().disabledTextColor),
4024
+ ...this.theme.backgroundColor(this.componentOptions().disabledColor),
4025
+ },
4026
+ '& input:disabled, select:disabled, textarea:disabled': {
4027
+ cursor: 'not-allowed !important',
4028
+ },
4029
+ '&:has(input:focus, select:focus, textarea:focus)': {
4030
+ outline: this.componentOptions().focusOutline,
4031
+ outlineOffset: this.componentOptions().focusOutlineOffset,
4032
+ // Optional focus chrome (border/ring/background). It yields to the
4033
+ // error state, so a flagged field stays visibly flagged while the
4034
+ // user is in it correcting the value.
4035
+ ...(this.error()
4036
+ ? {}
4037
+ : {
4038
+ ...this.theme.border(this.componentOptions().focusBorder),
4039
+ ...this.theme.boxShadow(this.componentOptions().focusShadow),
4040
+ ...this.theme.backgroundColor(this.componentOptions().focusColor),
4041
+ }),
4042
+ },
4043
+ },
4044
+ ]), ...(ngDevMode ? [{ debugName: "inputBoxClass" }] : /* istanbul ignore next */ []));
4045
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4046
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniInputBoxComponent, isStandalone: true, selector: "uni-input-box", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'input' }], usesInheritance: true, ngImport: i0, template: "<div\n row-layout\n alignItems=\"center\"\n [height]=\"height() ?? componentOptions().height\"\n [color]=\"color()\"\n [border]=\"border()\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [shadow]=\"shadow()\"\n [minWidth]=\"minWidth()\"\n [class]=\"inputBoxClass()\"\n position=\"relative\"\n>\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "component", type: UniRowComponent, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4047
+ }
4048
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, decorators: [{
4049
+ type: Component,
4050
+ args: [{ selector: 'uni-input-box', imports: [UniRowComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'input' }], host: { '[class]': 'className' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n row-layout\n alignItems=\"center\"\n [height]=\"height() ?? componentOptions().height\"\n [color]=\"color()\"\n [border]=\"border()\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [shadow]=\"shadow()\"\n [minWidth]=\"minWidth()\"\n [class]=\"inputBoxClass()\"\n position=\"relative\"\n>\n <ng-content></ng-content>\n</div>\n" }]
4051
+ }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
4052
+
4053
+ /**
4054
+ * Form-bound, closed-set, single-select autocomplete: `FormValueControl<T | null>`
4055
+ * over object `Options<T>`, type-to-filter, commit-on-select. Typing produces a
4056
+ * draft — a filter, never a value; the value changes only when an option
4057
+ * commits, `clear()` runs, or the model is written from outside. Select
4058
+ * semantics, not search: a real label, a chevron, no magnifier, nothing
4059
+ * submits. For short lists with no need to type, prefer `uni-select` — the
4060
+ * value contract is identical, so swapping is a template-only change.
4061
+ */
4062
+ class UniComboboxComponent extends BaseComponent {
4063
+ // --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
4064
+ value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
4065
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4066
+ touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
4067
+ invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
4068
+ dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
4069
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
4070
+ ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
4071
+ // --- Configuration -------------------------------------------------------
4072
+ /** Accessible name for the field, e.g. "State"; echoed on the listbox. */
4073
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
4074
+ placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
4075
+ options = input([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
4076
+ /**
4077
+ * Equality used to match `value` against option values, called as
4078
+ * `compareWith(optionValue, value)`. Defaults to reference equality, which
4079
+ * never matches an object value rebuilt from elsewhere (e.g. a saved record
4080
+ * against options from a fresh fetch) — pass a key comparison like
4081
+ * `(a, b) => a?.id === b?.id` for object values.
4082
+ */
4083
+ compareWith = input((a, b) => a === b, ...(ngDevMode ? [{ debugName: "compareWith" }] : /* istanbul ignore next */ []));
4084
+ width = input('100%', ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
4085
+ /** Show a ✕ while a value is set; an emptied field on blur also clears. */
4086
+ clearable = input(true, ...(ngDevMode ? [{ debugName: "clearable" }] : /* istanbul ignore next */ []));
4087
+ /** Blur commits an exact-match draft; a non-matching draft reverts. */
4088
+ commitOnBlur = input(true, ...(ngDevMode ? [{ debugName: "commitOnBlur" }] : /* istanbul ignore next */ []));
4089
+ /** i18n-able empty row, also announced when a filter matches nothing. */
4090
+ emptyText = input('No matches', ...(ngDevMode ? [{ debugName: "emptyText" }] : /* istanbul ignore next */ []));
4091
+ // --- Filtering (local by default — the closed set is already in memory) ---
4092
+ /** `false` renders `options` verbatim; narrow them app-side from `query`. */
4093
+ filterLocally = input(true, ...(ngDevMode ? [{ debugName: "filterLocally" }] : /* istanbul ignore next */ []));
4094
+ /** Filter predicate; default is locale-lowercased label-contains. */
4095
+ filterWith = input(...(ngDevMode ? [undefined, { debugName: "filterWith" }] : /* istanbul ignore next */ []));
4096
+ debounceTime = input(250, ...(ngDevMode ? [{ debugName: "debounceTime" }] : /* istanbul ignore next */ []));
4097
+ // --- Events ---------------------------------------------------------------
4098
+ /** Debounced draft text, for async option lists. */
4099
+ query = output();
4100
+ /** An option committed. */
4101
+ selected = output();
4102
+ cleared = output();
4103
+ /** A commit was refused (no match); the field reverted. */
4104
+ rejected = output();
4105
+ host = inject(ElementRef);
4106
+ inputRef = viewChild.required('field');
4107
+ listRef = viewChild('listbox', ...(ngDevMode ? [{ debugName: "listRef" }] : /* istanbul ignore next */ []));
4108
+ /** Cancelled on destroy — a late tick would emit on a destroyed OutputRef. */
4109
+ queryTimer;
4110
+ constructor() {
4111
+ super();
4112
+ inject(DestroyRef).onDestroy(() => clearTimeout(this.queryTimer));
4113
+ }
4114
+ srOnly = css(visuallyHidden);
4115
+ announcement = signal('', ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
4116
+ /** null → the field shows the committed label; a string is an uncommitted draft. */
4117
+ draft = signal(null, ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
4118
+ /**
4119
+ * Popup visibility. Component-owned rather than `list.open()` — the shared
4120
+ * signal gates on `count() > 0`, which would hide the "No matches" row and
4121
+ * misreport `aria-expanded` over an empty filter.
4122
+ */
4123
+ popupOpen = signal(false, ...(ngDevMode ? [{ debugName: "popupOpen" }] : /* istanbul ignore next */ []));
4124
+ /** Index in options() of the option matching value(), else -1. */
4125
+ committedIndex = computed(() => {
4126
+ const value = this.value();
4127
+ if (value === null || value === undefined)
4128
+ return -1;
4129
+ return this.options().findIndex((option) => this.compareWith()(option.value, value));
4130
+ }, ...(ngDevMode ? [{ debugName: "committedIndex" }] : /* istanbul ignore next */ []));
4131
+ /** A value with no matching option renders '' but is preserved — options may
4132
+ still be loading; the field self-heals when they arrive. */
4133
+ committedLabel = computed(() => {
4134
+ const index = this.committedIndex();
4135
+ return index >= 0 ? this.options()[index].label : '';
4136
+ }, ...(ngDevMode ? [{ debugName: "committedLabel" }] : /* istanbul ignore next */ []));
4137
+ displayValue = computed(() => this.draft() ?? this.committedLabel(), ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
4138
+ /** Indices into options() surviving the filter. The draft filters; the
4139
+ committed label does not — reopening always shows the full set. */
4140
+ filteredIndices = computed(() => {
4141
+ const query = this.draft();
4142
+ const all = this.options().map((_, index) => index);
4143
+ if (!this.filterLocally() || query === null || query === '')
4144
+ return all;
4145
+ const filter = this.filterWith() ??
4146
+ ((option, q) => option.label.toLocaleLowerCase().includes(q.toLocaleLowerCase()));
4147
+ return all.filter((index) => filter(this.options()[index], query));
4148
+ }, ...(ngDevMode ? [{ debugName: "filteredIndices" }] : /* istanbul ignore next */ []));
4149
+ /** Shared combobox bookkeeping, in filtered-list positions — the fourth
4150
+ consumer of the contract behind search-input, tag-input and time-input. */
4151
+ list = createListboxNavigation({
4152
+ count: () => this.filteredIndices().length,
4153
+ idPrefix: 'uni-combobox-listbox',
4154
+ disabled: (position) => !!this.options()[this.filteredIndices()[position]]?.disabled,
4155
+ });
4156
+ showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
4157
+ // --- Committing ------------------------------------------------------------
4158
+ commit(optIndex) {
4159
+ const option = this.options()[optIndex];
4160
+ if (!option || option.disabled)
4161
+ return false;
4162
+ this.value.set(option.value);
4163
+ this.draft.set(null);
4164
+ this.closeList();
4165
+ this.setFieldText(this.displayValue());
4166
+ this.announce(`${option.label} selected.`);
4167
+ this.selected.emit(option);
4168
+ return true;
4169
+ }
4170
+ clear() {
4171
+ this.value.set(null);
4172
+ this.draft.set(null);
4173
+ this.closeList();
4174
+ this.setFieldText('');
4175
+ this.announce('Selection cleared.');
4176
+ this.cleared.emit();
4177
+ this.inputRef().nativeElement.focus();
4178
+ }
4179
+ /**
4180
+ * Draft resolution, used by Enter, Tab and blur:
4181
+ * 1. an active option in the list → commit it;
4182
+ * 2. else a unique exact label match (locale-case-insensitive) → commit it;
4183
+ * 3. Enter only: the filter narrowed to exactly one enabled option → commit it
4184
+ * (on Enter the user can see the single candidate; on blur they're gone,
4185
+ * and committing a value they never saw confirmed is how forms grow
4186
+ * mystery data);
4187
+ * 4. else the caller keeps the list open (Enter) or reverts (Tab/blur).
4188
+ */
4189
+ resolveDraft(enterOnly) {
4190
+ const filtered = this.filteredIndices();
4191
+ const active = this.list.activeIndex();
4192
+ if (this.popupOpen() && active >= 0 && active < filtered.length) {
4193
+ return this.commit(filtered[active]);
4194
+ }
4195
+ const draft = this.draft();
4196
+ if (draft === null)
4197
+ return true;
4198
+ if (draft === '') {
4199
+ // An emptied field on Tab/blur is a deliberate "no value" — but only
4200
+ // clearable fields may null the model this way.
4201
+ if (this.clearable() && this.value() !== null && !enterOnly) {
4202
+ this.value.set(null);
4203
+ this.announce('Selection cleared.');
4204
+ this.cleared.emit();
4205
+ }
4206
+ this.draft.set(null);
4207
+ this.setFieldText(this.displayValue());
4208
+ return true;
4209
+ }
4210
+ const normalize = (text) => text.toLocaleLowerCase();
4211
+ const exact = this.options()
4212
+ .map((option, index) => ({ option, index }))
4213
+ .filter(({ option }) => !option.disabled && normalize(option.label) === normalize(draft));
4214
+ if (exact.length === 1)
4215
+ return this.commit(exact[0].index);
4216
+ if (enterOnly) {
4217
+ const enabled = filtered.filter((index) => !this.options()[index].disabled);
4218
+ if (enabled.length === 1)
4219
+ return this.commit(enabled[0]);
4220
+ }
4221
+ return false;
4222
+ }
4223
+ revertDraft() {
4224
+ const draft = this.draft();
4225
+ if (draft === null)
4226
+ return null;
4227
+ this.draft.set(null);
4228
+ this.setFieldText(this.displayValue());
4229
+ return draft;
4230
+ }
4231
+ reject() {
4232
+ const query = this.revertDraft();
4233
+ if (query) {
4234
+ this.announce(`No match for “${query}”.`);
4235
+ this.rejected.emit({ query });
4236
+ }
4237
+ }
4238
+ // --- Keyboard --------------------------------------------------------------
4239
+ onKeydown(event) {
4240
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
4241
+ event.preventDefault();
4242
+ if (event.altKey) {
4243
+ // Alt+Down opens without activating; Alt+Up closes keeping the draft.
4244
+ if (event.key === 'ArrowDown') {
4245
+ if (!this.popupOpen())
4246
+ this.openList(-1);
4247
+ }
4248
+ else {
4249
+ this.closeList();
4250
+ }
4251
+ return;
4252
+ }
4253
+ if (!this.popupOpen()) {
4254
+ this.popupOpen.set(true);
4255
+ this.list.show();
4256
+ if (event.key === 'ArrowDown') {
4257
+ const committed = this.committedFilteredPos();
4258
+ if (committed >= 0) {
4259
+ this.list.setActive(committed);
4260
+ this.scrollToActive();
4261
+ return;
4262
+ }
4263
+ }
4264
+ // Fall through: Down lands on the first enabled option, Up on the last.
4265
+ }
4266
+ if (this.list.navigate(event))
4267
+ this.scrollToActive();
4268
+ return;
4269
+ }
4270
+ if (event.key === 'Home' || event.key === 'End') {
4271
+ // Only while the list is open — closed, they belong to the caret.
4272
+ if (this.popupOpen() && this.list.navigate(event))
4273
+ this.scrollToActive();
4274
+ return;
4275
+ }
4276
+ switch (event.key) {
4277
+ case 'Enter':
4278
+ // Never submits a form while the list is open.
4279
+ if (this.popupOpen())
4280
+ event.preventDefault();
4281
+ if (!this.resolveDraft(true)) {
4282
+ const count = this.filteredIndices().length;
4283
+ this.announce(count === 0 ? `${this.emptyText()}.` : `${count} results. Use the arrow keys.`);
4284
+ }
4285
+ return;
4286
+ case 'Escape':
4287
+ // One layer at a time: close the list, then revert the draft.
4288
+ // Never clears the committed value — Escape on a form control must
4289
+ // not be destructive.
4290
+ if (this.popupOpen())
4291
+ this.closeList();
4292
+ else if (this.draft() !== null)
4293
+ this.revertDraft();
4294
+ return;
4295
+ case 'Tab':
4296
+ // Resolve rules 1–2 (never rule 3), then let focus move on.
4297
+ if (this.commitOnBlur() && !this.resolveDraft(false))
4298
+ this.reject();
4299
+ this.closeList();
4300
+ }
4301
+ }
4302
+ onInput() {
4303
+ const text = this.inputRef().nativeElement.value;
4304
+ this.draft.set(text);
4305
+ this.popupOpen.set(true);
4306
+ this.list.show();
4307
+ // Typing never selects — rules 2/3 make Enter still work without arrowing.
4308
+ this.list.setActive(-1);
4309
+ clearTimeout(this.queryTimer);
4310
+ this.queryTimer = setTimeout(() => {
4311
+ this.query.emit(text);
4312
+ // Filtering is otherwise silent to a screen reader.
4313
+ if (this.filterLocally() && text !== '') {
4314
+ const count = this.filteredIndices().length;
4315
+ this.announce(count === 0 ? `${this.emptyText()}.` : `${count} result${count === 1 ? '' : 's'}.`);
4316
+ }
4317
+ }, this.debounceTime());
4318
+ }
4319
+ // --- Pointer ---------------------------------------------------------------
4320
+ /** Click-to-browse: pointer users get the select affordance. Focus alone
4321
+ never opens — Tab-through forms must not spray popups. */
4322
+ onFieldClick() {
4323
+ if (this.disabled() || this.popupOpen())
4324
+ return;
4325
+ this.openList(this.committedFilteredPos());
4326
+ }
4327
+ onToggleMousedown(event) {
4328
+ event.preventDefault();
4329
+ if (this.disabled())
4330
+ return;
4331
+ this.inputRef().nativeElement.focus();
4332
+ if (this.popupOpen())
4333
+ this.closeList();
4334
+ else
4335
+ this.openList(this.committedFilteredPos());
4336
+ }
4337
+ onOptionClick(optIndex) {
4338
+ if (this.options()[optIndex]?.disabled)
4339
+ return;
4340
+ this.commit(optIndex);
4341
+ this.inputRef().nativeElement.focus();
4342
+ }
4343
+ onFocusOut(event) {
4344
+ const next = event.relatedTarget;
4345
+ if (next && this.host.nativeElement.contains(next))
4346
+ return;
4347
+ this.touched.set(true);
4348
+ if (this.commitOnBlur() && !this.resolveDraft(false))
4349
+ this.reject();
4350
+ else
4351
+ this.draft.set(null);
4352
+ this.closeList();
4353
+ }
4354
+ // --- Internals -------------------------------------------------------------
4355
+ /** Filtered position of the committed option when visible and enabled, else -1. */
4356
+ committedFilteredPos() {
4357
+ const committed = this.committedIndex();
4358
+ if (committed < 0 || this.options()[committed].disabled)
4359
+ return -1;
4360
+ return this.filteredIndices().indexOf(committed);
4361
+ }
4362
+ openList(activeFilteredPos) {
4363
+ this.popupOpen.set(true);
4364
+ this.list.show();
4365
+ this.list.setActive(activeFilteredPos);
4366
+ this.scrollToActive();
4367
+ }
4368
+ closeList() {
4369
+ this.popupOpen.set(false);
4370
+ this.list.hide();
4371
+ }
4372
+ scrollToActive() {
4373
+ const position = this.list.activeIndex();
4374
+ if (position < 0)
4375
+ return;
4376
+ queueMicrotask(() => this.listRef()?.nativeElement.children[position]?.scrollIntoView?.({ block: 'nearest' }));
4377
+ }
4378
+ setFieldText(text) {
4379
+ const element = this.inputRef()?.nativeElement;
4380
+ if (element)
4381
+ element.value = text;
4382
+ }
4383
+ announce(message) {
4384
+ // Re-announce identical text by breaking the string equality.
4385
+ this.announcement.set(this.announcement() === message ? `${message} ` : message);
4386
+ }
4387
+ // --- Styling ----------------------------------------------------------------
4388
+ className = computed(() => css({ display: 'block', position: 'relative', width: this.width() }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
4389
+ rowClass = computed(() => css({ display: 'flex', alignItems: 'center', flex: 1, width: '100%', minWidth: 0 }), ...(ngDevMode ? [{ debugName: "rowClass" }] : /* istanbul ignore next */ []));
4390
+ inputClass = computed(() => css({
4391
+ flex: 1,
4392
+ minWidth: 0,
4393
+ border: 0,
4394
+ outline: 'none',
4395
+ background: 'transparent',
4396
+ color: 'inherit',
4397
+ font: 'inherit',
4398
+ }), ...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
4399
+ /** Pointer-only affordance: keyboard already has ArrowDown, and the input
4400
+ itself announces expanded state. */
4401
+ toggleClass = computed(() => css({
4402
+ flex: 'none',
4403
+ width: 28,
4404
+ height: 28,
4405
+ display: 'grid',
4406
+ placeItems: 'center',
4407
+ border: 0,
4408
+ padding: 0,
4409
+ background: 'transparent',
4410
+ cursor: this.disabled() ? 'not-allowed' : 'pointer',
4411
+ ...this.theme.color('on-background-variant'),
4412
+ '& uni-icon': {
4413
+ ...motionSafe({ transition: 'transform 0.15s ease' }),
4414
+ transform: this.popupOpen() ? 'rotate(180deg)' : 'none',
4415
+ },
4416
+ }), ...(ngDevMode ? [{ debugName: "toggleClass" }] : /* istanbul ignore next */ []));
4417
+ listClass = computed(() => {
4418
+ const options = this.componentOptions();
4419
+ return css([
4420
+ listboxPopupStyles(this.theme, options, {
4421
+ // A scroll height, never a cap: a closed-set control must not render a
4422
+ // reachable-by-keyboard-only subset (contrast searchInput.maxSuggestions).
4423
+ maxHeight: (options.maxVisibleOptions ?? 8) * 36 + 8,
4424
+ }),
4425
+ {
4426
+ '& [role="option"]': {
4427
+ display: 'flex',
4428
+ alignItems: 'center',
4429
+ gap: 10,
4430
+ minHeight: 36,
4431
+ boxSizing: 'border-box',
4432
+ padding: '5px 10px',
4433
+ '&.active, &:not([aria-disabled="true"]):hover': {
4434
+ '& .check': { color: 'inherit' },
4435
+ '& .desc': { color: 'inherit', opacity: 0.8 },
4436
+ },
4437
+ '&[aria-disabled="true"]': {
4438
+ ...this.theme.color('on-disabled'),
4439
+ cursor: 'not-allowed',
4440
+ '& .desc': { color: 'inherit' },
4441
+ },
4442
+ },
4443
+ // Fixed-width check column so labels align with or without the icon.
4444
+ '& .check': { flex: 'none', width: 18, ...this.theme.color('primary') },
4445
+ '& .text': { minWidth: 0, flex: 1, display: 'flex', alignItems: 'baseline', gap: 8 },
4446
+ '& .option-label': { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
4447
+ '& .desc': {
4448
+ marginLeft: 'auto',
4449
+ whiteSpace: 'nowrap',
4450
+ fontSize: '0.85em',
4451
+ ...this.theme.color(options.descriptionColor ?? 'on-primary-surface-variant'),
4452
+ },
4453
+ '& .empty': {
4454
+ padding: 10,
4455
+ ...this.theme.typeface('label'),
4456
+ ...this.theme.color(options.descriptionColor ?? 'on-primary-surface-variant'),
4457
+ },
4458
+ },
4459
+ ]);
4460
+ }, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
4461
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniComboboxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4462
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniComboboxComponent, isStandalone: true, selector: "uni-combobox, Combobox", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, commitOnBlur: { classPropertyName: "commitOnBlur", publicName: "commitOnBlur", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, filterLocally: { classPropertyName: "filterLocally", publicName: "filterLocally", isSignal: true, isRequired: false, transformFunction: null }, filterWith: { classPropertyName: "filterWith", publicName: "filterWith", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", query: "query", selected: "selected", cleared: "cleared", rejected: "rejected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'combobox' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["field"], descendants: true, isSignal: true }, { propertyName: "listRef", first: true, predicate: ["listbox"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Focusout rides the bubbling event so the list closes and the draft\n resolves only when focus leaves the whole field. -->\n<div (focusout)=\"onFocusOut($event)\">\n <uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div [class]=\"rowClass()\">\n <input\n #field\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n aria-autocomplete=\"list\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder() ?? ''\"\n [value]=\"displayValue()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-expanded]=\"popupOpen()\"\n [attr.aria-controls]=\"list.listboxId\"\n [attr.aria-activedescendant]=\"list.activeDescendantId()\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onKeydown($event)\"\n (input)=\"onInput()\"\n (click)=\"onFieldClick()\"\n />\n @if (clearable() && value() !== null && !disabled()) {\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n [iconName]=\"componentOptions().clearIcon ?? 'close'\"\n (click)=\"clear()\"\n >\n Clear {{ label() }}\n </button>\n }\n <!-- Pointer-only: keyboard has ArrowDown, the input announces expanded. -->\n <button\n type=\"button\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n [class]=\"toggleClass()\"\n [disabled]=\"disabled()\"\n (mousedown)=\"onToggleMousedown($event)\"\n >\n <uni-icon [name]=\"componentOptions().toggleIcon ?? 'chevronDown'\" size=\"20\" />\n </button>\n </div>\n </uni-input-box>\n\n @if (popupOpen()) {\n <ul #listbox [id]=\"list.listboxId\" role=\"listbox\" [attr.aria-label]=\"label()\" [class]=\"listClass()\">\n @if (filteredIndices().length === 0) {\n <!-- Non-interactive: not an option, not navigable, no id. -->\n <li class=\"empty\">{{ emptyText() }}</li>\n } @else {\n @for (optIndex of filteredIndices(); track optIndex; let i = $index) {\n <!-- aria-selected marks the *committed* option; the active one is\n carried by aria-activedescendant \u2014 they are different facts. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"optIndex === committedIndex()\"\n [attr.aria-disabled]=\"options()[optIndex].disabled ? true : null\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(optIndex)\"\n >\n <span class=\"check\" aria-hidden=\"true\">\n @if (optIndex === committedIndex()) {\n <uni-icon [name]=\"componentOptions().selectedIcon ?? 'check'\" size=\"18\" />\n }\n </span>\n <span class=\"text\">\n <span class=\"option-label\">{{ options()[optIndex].label }}</span>\n @if (options()[optIndex].description) {\n <span class=\"desc\">{{ options()[optIndex].description }}</span>\n }\n </span>\n </li>\n }\n }\n </ul>\n }\n\n <!-- Commits, clears, refusals and result counts are otherwise silent. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n</div>\n", dependencies: [{ kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniIconComponent, selector: "uni-icon", inputs: ["color", "name", "size"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4463
+ }
4464
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniComboboxComponent, decorators: [{
4465
+ type: Component,
4466
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-combobox, Combobox', imports: [UniIconButtonComponent, UniIconComponent, UniInputBoxComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'combobox' }], host: { '[class]': 'className()' }, template: "<!-- Focusout rides the bubbling event so the list closes and the draft\n resolves only when focus leaves the whole field. -->\n<div (focusout)=\"onFocusOut($event)\">\n <uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div [class]=\"rowClass()\">\n <input\n #field\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n aria-autocomplete=\"list\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder() ?? ''\"\n [value]=\"displayValue()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-expanded]=\"popupOpen()\"\n [attr.aria-controls]=\"list.listboxId\"\n [attr.aria-activedescendant]=\"list.activeDescendantId()\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onKeydown($event)\"\n (input)=\"onInput()\"\n (click)=\"onFieldClick()\"\n />\n @if (clearable() && value() !== null && !disabled()) {\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n [iconName]=\"componentOptions().clearIcon ?? 'close'\"\n (click)=\"clear()\"\n >\n Clear {{ label() }}\n </button>\n }\n <!-- Pointer-only: keyboard has ArrowDown, the input announces expanded. -->\n <button\n type=\"button\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n [class]=\"toggleClass()\"\n [disabled]=\"disabled()\"\n (mousedown)=\"onToggleMousedown($event)\"\n >\n <uni-icon [name]=\"componentOptions().toggleIcon ?? 'chevronDown'\" size=\"20\" />\n </button>\n </div>\n </uni-input-box>\n\n @if (popupOpen()) {\n <ul #listbox [id]=\"list.listboxId\" role=\"listbox\" [attr.aria-label]=\"label()\" [class]=\"listClass()\">\n @if (filteredIndices().length === 0) {\n <!-- Non-interactive: not an option, not navigable, no id. -->\n <li class=\"empty\">{{ emptyText() }}</li>\n } @else {\n @for (optIndex of filteredIndices(); track optIndex; let i = $index) {\n <!-- aria-selected marks the *committed* option; the active one is\n carried by aria-activedescendant \u2014 they are different facts. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"optIndex === committedIndex()\"\n [attr.aria-disabled]=\"options()[optIndex].disabled ? true : null\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(optIndex)\"\n >\n <span class=\"check\" aria-hidden=\"true\">\n @if (optIndex === committedIndex()) {\n <uni-icon [name]=\"componentOptions().selectedIcon ?? 'check'\" size=\"18\" />\n }\n </span>\n <span class=\"text\">\n <span class=\"option-label\">{{ options()[optIndex].label }}</span>\n @if (options()[optIndex].description) {\n <span class=\"desc\">{{ options()[optIndex].description }}</span>\n }\n </span>\n </li>\n }\n }\n </ul>\n }\n\n <!-- Commits, clears, refusals and result counts are otherwise silent. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n</div>\n" }]
4467
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], commitOnBlur: [{ type: i0.Input, args: [{ isSignal: true, alias: "commitOnBlur", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], filterLocally: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterLocally", required: false }] }], filterWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterWith", required: false }] }], debounceTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceTime", required: false }] }], query: [{ type: i0.Output, args: ["query"] }], selected: [{ type: i0.Output, args: ["selected"] }], cleared: [{ type: i0.Output, args: ["cleared"] }], rejected: [{ type: i0.Output, args: ["rejected"] }], inputRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], listRef: [{ type: i0.ViewChild, args: ['listbox', { isSignal: true }] }] } });
4468
+
3345
4469
  class UniDataSearchComponent extends BaseComponent {
3346
4470
  datasource = input(...(ngDevMode ? [undefined, { debugName: "datasource" }] : /* istanbul ignore next */ []));
3347
4471
  placeholder = input('Search', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
@@ -3857,6 +4981,9 @@ class UniDropdownComponent extends BaseComponent {
3857
4981
  get _dropdown() {
3858
4982
  return this.dropdownRef.nativeElement;
3859
4983
  }
4984
+ // Pre-measure default only: the requested placement's corner. The real
4985
+ // origin is measured per toggle (syncTransformOrigin), because
4986
+ // position-try fallbacks may have flipped the panel.
3860
4987
  transformOriginMap = {
3861
4988
  top: 'bottom center',
3862
4989
  right: 'center left',
@@ -3930,6 +5057,10 @@ class UniDropdownComponent extends BaseComponent {
3930
5057
  // Sync state if user invokes light-dismiss via outside click or Escape key
3931
5058
  this.renderer.listen(this._dropdown, 'toggle', (event) => {
3932
5059
  const isOpened = event.newState === 'open';
5060
+ // Both edges: on open so the entry scale grows out of the trigger, and
5061
+ // on close-start so a panel the browser flipped while open (scroll near
5062
+ // a viewport edge) still collapses back toward the trigger.
5063
+ this.syncTransformOrigin();
3933
5064
  this.showing.set(isOpened);
3934
5065
  this.renderer.setAttribute(this._focusTarget, 'aria-expanded', `${isOpened}`);
3935
5066
  if (isOpened) {
@@ -3941,6 +5072,19 @@ class UniDropdownComponent extends BaseComponent {
3941
5072
  }
3942
5073
  });
3943
5074
  }
5075
+ /**
5076
+ * Scale the open/close animation from the corner touching the trigger,
5077
+ * wherever the browser actually placed the panel. The static
5078
+ * `transformOriginMap` covers only the *requested* placement; with
5079
+ * `position-try-fallbacks` the panel may have flipped at a viewport edge,
5080
+ * and a `bottom-end` picker rendered above its field would otherwise still
5081
+ * animate from the top-right corner.
5082
+ */
5083
+ syncTransformOrigin() {
5084
+ const origin = transformOriginFor(this._dropdown.getBoundingClientRect(), this._trigger.getBoundingClientRect());
5085
+ if (origin)
5086
+ this.renderer.setStyle(this._dropdown, 'transform-origin', origin);
5087
+ }
3944
5088
  /**
3945
5089
  * Returns focus to the trigger when the popover closes while focus was
3946
5090
  * inside it (or was dropped on <body> by the top layer closing), so
@@ -4018,69 +5162,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
4018
5162
  args: ['dropdown', { static: true }]
4019
5163
  }] } });
4020
5164
 
4021
- class UniInputBoxComponent extends BaseComponent {
4022
- className = css({ display: 'contents' });
4023
- disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4024
- error = input(false, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
4025
- minWidth = input('0', ...(ngDevMode ? [{ debugName: "minWidth" }] : /* istanbul ignore next */ []));
4026
- /** Override the themed field height, e.g. `'auto'` for multi-line fields. */
4027
- height = input(undefined, ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
4028
- color = computed(() => this.error() ? this.componentOptions().errorColor : this.componentOptions().color, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
4029
- border = computed(() => this.error() ? this.componentOptions().errorBorder : this.componentOptions().border, ...(ngDevMode ? [{ debugName: "border" }] : /* istanbul ignore next */ []));
4030
- shadow = computed(() => this.error() ? this.componentOptions().errorShadow : this.componentOptions().shadow, ...(ngDevMode ? [{ debugName: "shadow" }] : /* istanbul ignore next */ []));
4031
- inputBoxClass = computed(() => css([
4032
- this.disabled() && {
4033
- ...this.theme.color(this.componentOptions().disabledTextColor),
4034
- ...this.theme.backgroundColor(this.componentOptions().disabledColor),
4035
- cursor: 'not-allowed !important',
4036
- },
4037
- {
4038
- '& input, select, textarea': {
4039
- ...removeInputPlatformStyling,
4040
- height: '100%',
4041
- ...this.theme.paddingLeft(this.componentOptions().paddingLeft),
4042
- ...this.theme.color(this.componentOptions().textColor),
4043
- // `typeFace` is the deprecated casing; themes that still set it win
4044
- // only when the canonical key is absent.
4045
- ...this.theme.typeface(this.componentOptions().typeface ?? this.componentOptions().typeFace),
4046
- },
4047
- // Multi-line fields size themselves (rows/resize), not from the box.
4048
- '& textarea': {
4049
- height: 'auto',
4050
- ...this.theme.paddingTop('xs'),
4051
- ...this.theme.paddingBottom('xs'),
4052
- },
4053
- '&:has(input:disabled, select:disabled, textarea:disabled)': {
4054
- ...this.theme.color(this.componentOptions().disabledTextColor),
4055
- ...this.theme.backgroundColor(this.componentOptions().disabledColor),
4056
- },
4057
- '& input:disabled, select:disabled, textarea:disabled': {
4058
- cursor: 'not-allowed !important',
4059
- },
4060
- '&:has(input:focus, select:focus, textarea:focus)': {
4061
- outline: this.componentOptions().focusOutline,
4062
- outlineOffset: this.componentOptions().focusOutlineOffset,
4063
- // Optional focus chrome (border/ring/background). It yields to the
4064
- // error state, so a flagged field stays visibly flagged while the
4065
- // user is in it correcting the value.
4066
- ...(this.error()
4067
- ? {}
4068
- : {
4069
- ...this.theme.border(this.componentOptions().focusBorder),
4070
- ...this.theme.boxShadow(this.componentOptions().focusShadow),
4071
- ...this.theme.backgroundColor(this.componentOptions().focusColor),
4072
- }),
4073
- },
4074
- },
4075
- ]), ...(ngDevMode ? [{ debugName: "inputBoxClass" }] : /* istanbul ignore next */ []));
4076
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4077
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniInputBoxComponent, isStandalone: true, selector: "uni-input-box", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'input' }], usesInheritance: true, ngImport: i0, template: "<div\n row-layout\n alignItems=\"center\"\n [height]=\"height() ?? componentOptions().height\"\n [color]=\"color()\"\n [border]=\"border()\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [shadow]=\"shadow()\"\n [minWidth]=\"minWidth()\"\n [class]=\"inputBoxClass()\"\n position=\"relative\"\n>\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "component", type: UniRowComponent, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4078
- }
4079
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, decorators: [{
4080
- type: Component,
4081
- args: [{ selector: 'uni-input-box', imports: [UniRowComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'input' }], host: { '[class]': 'className' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n row-layout\n alignItems=\"center\"\n [height]=\"height() ?? componentOptions().height\"\n [color]=\"color()\"\n [border]=\"border()\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [shadow]=\"shadow()\"\n [minWidth]=\"minWidth()\"\n [class]=\"inputBoxClass()\"\n position=\"relative\"\n>\n <ng-content></ng-content>\n</div>\n" }]
4082
- }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
4083
-
4084
5165
  /**
4085
5166
  * Date field with free-typed parsing and a popup calendar. Type `aug 20`,
4086
5167
  * `8/20/2026` or `2026-08-20`, or pick from the grid — the form gets the
@@ -4671,32 +5752,9 @@ class UniTimeInputComponent extends BaseComponent {
4671
5752
  embeddedClass = computed(() => css({ display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 }), ...(ngDevMode ? [{ debugName: "embeddedClass" }] : /* istanbul ignore next */ []));
4672
5753
  listClass = computed(() => {
4673
5754
  const options = this.componentOptions();
4674
- return css({
4675
- position: 'absolute',
4676
- top: '100%',
4677
- left: 0,
4678
- right: 0,
4679
- zIndex: 20,
4680
- margin: '4px 0 0',
4681
- padding: 4,
4682
- listStyle: 'none',
5755
+ return css(listboxPopupStyles(this.theme, options, {
4683
5756
  maxHeight: (options.maxVisibleOptions ?? 7) * 36,
4684
- overflowY: 'auto',
4685
- ...this.theme.backgroundColor(options.listColor ?? 'primary-surface'),
4686
- ...this.theme.boxShadow(options.listShadow ?? 'menu'),
4687
- ...this.theme.radius(options.listBorderRadius ?? 'xs'),
4688
- '& [role="option"]': {
4689
- padding: '8px 12px',
4690
- cursor: 'pointer',
4691
- ...this.theme.typeface('label'),
4692
- ...this.theme.color('on-primary-surface'),
4693
- ...this.theme.radius('xxs'),
4694
- '&.active, &:hover': {
4695
- ...this.theme.backgroundColor('primary-container'),
4696
- ...this.theme.color('on-primary-container'),
4697
- },
4698
- },
4699
- });
5757
+ }));
4700
5758
  }, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
4701
5759
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTimeInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4702
5760
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniTimeInputComponent, isStandalone: true, selector: "uni-time-input, TimeInput", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, minuteStep: { classPropertyName: "minuteStep", publicName: "minuteStep", isSignal: true, isRequired: false, transformFunction: null }, minTime: { classPropertyName: "minTime", publicName: "minTime", isSignal: true, isRequired: false, transformFunction: null }, maxTime: { classPropertyName: "maxTime", publicName: "maxTime", isSignal: true, isRequired: false, transformFunction: null }, slots: { classPropertyName: "slots", publicName: "slots", isSignal: true, isRequired: false, transformFunction: null }, hour12: { classPropertyName: "hour12", publicName: "hour12", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, commitOnBlur: { classPropertyName: "commitOnBlur", publicName: "commitOnBlur", isSignal: true, isRequired: false, transformFunction: null }, embedded: { classPropertyName: "embedded", publicName: "embedded", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", rejected: "rejected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'timeInput' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["field"], descendants: true, isSignal: true }, { propertyName: "listRef", first: true, predicate: ["listbox"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Focusout rides the bubbling event so the list closes and the draft\n commits only when focus leaves the whole field. -->\n<div (focusout)=\"onFocusOut($event)\">\n <ng-template #parts>\n <input\n #field\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n aria-autocomplete=\"list\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"displayText()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-expanded]=\"list.open()\"\n [attr.aria-controls]=\"list.listboxId\"\n [attr.aria-activedescendant]=\"list.activeDescendantId()\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() || draftInvalid() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput()\"\n />\n <span [class]=\"toggleWrapClass()\">\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n aria-haspopup=\"listbox\"\n [symbolName]=\"componentOptions().toggleSymbol ?? 'schedule'\"\n [attr.aria-expanded]=\"list.open()\"\n [disable]=\"disabled()\"\n (click)=\"onToggle()\"\n >\n Choose time\n </button>\n </span>\n </ng-template>\n\n @if (embedded()) {\n <!-- A composer (uni-date-time-input) owns the field chrome. -->\n <div [class]=\"embeddedClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n } @else {\n <uni-input-box [error]=\"showError() || draftInvalid()\" [disabled]=\"disabled()\">\n <div [class]=\"rowClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n </uni-input-box>\n }\n\n @if (list.open()) {\n <ul #listbox [id]=\"list.listboxId\" role=\"listbox\" [attr.aria-label]=\"label()\" [class]=\"listClass()\">\n @for (time of options(); track time; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"time === value()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"selectOption(time)\"\n >\n {{ optionLabels()[i] }}\n </li>\n }\n </ul>\n }\n\n <!-- Commits and rejections are otherwise a silent reformat. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n</div>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
@@ -6829,34 +7887,121 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
6829
7887
  * This file exports all public-facing elements of the paginator component.
6830
7888
  */
6831
7889
 
6832
- class UniPopoverComponent {
7890
+ /**
7891
+ * Anchored, top-layer panel on the native `popover` element — the browser owns
7892
+ * positioning (CSS anchor positioning), stacking, and, in rich mode, light
7893
+ * dismissal. Two modes share one implementation:
7894
+ *
7895
+ * - `rich` (default): a click-toggled disclosure. The projected `[trigger]`
7896
+ * carries `aria-expanded`/`aria-controls`; with no trigger content the app
7897
+ * drives `open` and no element claims controller ARIA. Focus stays on the
7898
+ * trigger (APG disclosure) unless the panel marks an `[autofocus]` field,
7899
+ * and returns to the trigger on close.
7900
+ * - `tooltip`: hover/focus-triggered, `role="tooltip"` + `aria-describedby`,
7901
+ * WCAG 1.4.13 dismissable/hoverable/persistent. Content must not contain
7902
+ * focusable elements.
7903
+ *
7904
+ * Anchoring and control are separate: `anchor` re-anchors the panel to any
7905
+ * element (or id) while the trigger keeps the disclosure semantics.
7906
+ */
7907
+ class UniPopoverComponent extends BaseComponent {
6833
7908
  renderer = inject(Renderer2);
6834
- theme = inject(ThemeService);
7909
+ destroyRef = inject(DestroyRef);
6835
7910
  placement = input('bottom', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
6836
7911
  /** Light-dismiss on outside click / Escape (native popover="auto"). */
6837
7912
  autoClose = input(true, ...(ngDevMode ? [{ debugName: "autoClose" }] : /* istanbul ignore next */ []));
7913
+ /** Two-way open state; light dismissal and hover timers sync back into it. */
7914
+ open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
7915
+ /** `rich` = click-toggled disclosure; `tooltip` = hover/focus description. */
7916
+ mode = input('rich', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
7917
+ /**
7918
+ * Anchor the panel to another element (or element id, resolved each time
7919
+ * the panel opens) while the projected trigger keeps the disclosure ARIA.
7920
+ * Empty string means unset.
7921
+ */
7922
+ anchor = input(...(ngDevMode ? [undefined, { debugName: "anchor" }] : /* istanbul ignore next */ []));
7923
+ /** Title rendered in the header row and used as the accessible name. */
7924
+ header = input(...(ngDevMode ? [undefined, { debugName: "header" }] : /* istanbul ignore next */ []));
7925
+ /** Renders a "Close" icon button in the header row. */
7926
+ closable = input(false, ...(ngDevMode ? [{ debugName: "closable" }] : /* istanbul ignore next */ []));
7927
+ arrow = input(true, ...(ngDevMode ? [{ debugName: "arrow" }] : /* istanbul ignore next */ []));
7928
+ /** Panel max-width; numbers are px. Defaults to the theme's `maxWidth`. */
7929
+ maxWidth = input(...(ngDevMode ? [undefined, { debugName: "maxWidth" }] : /* istanbul ignore next */ []));
7930
+ /** Tooltip-mode hover-open delay, ms; defaults to the theme option. */
7931
+ openDelay = input(...(ngDevMode ? [undefined, { debugName: "openDelay" }] : /* istanbul ignore next */ []));
7932
+ /** Tooltip-mode pointer-leave close delay, ms; defaults to the theme option. */
7933
+ closeDelay = input(...(ngDevMode ? [undefined, { debugName: "closeDelay" }] : /* istanbul ignore next */ []));
7934
+ opened = output();
7935
+ closed = output();
6838
7936
  panelId = uniqueId('uni-popover');
7937
+ headerId = uniqueId('uni-popover-header');
6839
7938
  anchorName = newAnchorName();
6840
7939
  showing = signal(false, ...(ngDevMode ? [{ debugName: "showing" }] : /* istanbul ignore next */ []));
7940
+ /** Whether the `[trigger]` slot projected any element content. */
7941
+ hasTrigger = signal(false, ...(ngDevMode ? [{ debugName: "hasTrigger" }] : /* istanbul ignore next */ []));
6841
7942
  triggerRef = viewChild.required('trigger');
6842
7943
  panelRef = viewChild.required('panel');
7944
+ openTimer = useTimer();
7945
+ closeTimer = useTimer();
7946
+ /** The element currently carrying our anchor-name. */
7947
+ anchoredEl = null;
7948
+ detachEscape = null;
6843
7949
  constructor() {
6844
- // Wire the ARIA disclosure contract onto the focusable trigger element
7950
+ super();
6845
7951
  afterNextRender(() => {
7952
+ this.hasTrigger.set(this.triggerRef().nativeElement.childElementCount > 0);
7953
+ this.applyAnchor();
7954
+ });
7955
+ // Controller ARIA follows mode/open state; each branch removes the other
7956
+ // mode's attributes so a runtime mode flip never leaves stale semantics.
7957
+ effect(() => {
7958
+ if (!this.hasTrigger())
7959
+ return;
6846
7960
  const target = resolveFocusTarget(this.triggerRef().nativeElement);
6847
- this.renderer.setAttribute(target, 'aria-expanded', 'false');
6848
- this.renderer.setAttribute(target, 'aria-controls', this.panelId);
7961
+ if (this.mode() === 'tooltip') {
7962
+ this.renderer.removeAttribute(target, 'aria-expanded');
7963
+ this.renderer.removeAttribute(target, 'aria-controls');
7964
+ this.renderer.setAttribute(target, 'aria-describedby', this.panelId);
7965
+ }
7966
+ else {
7967
+ this.renderer.removeAttribute(target, 'aria-describedby');
7968
+ this.renderer.setAttribute(target, 'aria-expanded', `${this.showing()}`);
7969
+ this.renderer.setAttribute(target, 'aria-controls', this.panelId);
7970
+ }
7971
+ });
7972
+ // Two-way `open`: drive the native popover when the model changes hands.
7973
+ effect(() => {
7974
+ const want = this.open();
7975
+ if (want === untracked(this.showing))
7976
+ return;
7977
+ if (want)
7978
+ this.showPopover();
7979
+ else
7980
+ this.hidePopover();
7981
+ });
7982
+ this.destroyRef.onDestroy(() => {
7983
+ this.detachEscape?.();
7984
+ if (this.anchoredEl)
7985
+ clearAnchorName(this.anchoredEl);
7986
+ try {
7987
+ this.panelRef().nativeElement.hidePopover();
7988
+ }
7989
+ catch {
7990
+ // popover was already closed or detached
7991
+ }
6849
7992
  });
6850
7993
  }
6851
- onToggle(event) {
6852
- const open = event.newState === 'open';
6853
- this.showing.set(open);
6854
- this.renderer.setAttribute(resolveFocusTarget(this.triggerRef().nativeElement), 'aria-expanded', `${open}`);
6855
- }
7994
+ /** Tooltip mode is always `manual` — its lifecycle belongs to the timers. */
7995
+ popoverKind = computed(() => this.mode() === 'tooltip' ? 'manual' : this.autoClose() ? 'auto' : 'manual', ...(ngDevMode ? [{ debugName: "popoverKind" }] : /* istanbul ignore next */ []));
6856
7996
  showPopover() {
7997
+ if (this.showing())
7998
+ return;
7999
+ this.applyAnchor();
6857
8000
  this.panelRef().nativeElement.showPopover();
6858
8001
  }
6859
8002
  hidePopover() {
8003
+ if (!this.showing())
8004
+ return;
6860
8005
  this.panelRef().nativeElement.hidePopover();
6861
8006
  }
6862
8007
  togglePopover(event) {
@@ -6866,60 +8011,198 @@ class UniPopoverComponent {
6866
8011
  else {
6867
8012
  this.showPopover();
6868
8013
  }
6869
- event.stopPropagation();
8014
+ event?.stopPropagation();
6870
8015
  }
6871
- triggerClassName = css({
6872
- display: 'inline-block',
6873
- anchorName: this.anchorName,
6874
- });
6875
- popoverClassName = computed(() => css([
6876
- {
6877
- ...this.theme.colorPair('primary-surface'),
6878
- ...this.theme.radius('xs'),
6879
- ...this.theme.boxShadow('raised'),
6880
- ...this.theme.typeface('label'),
6881
- ...this.theme.border('quaternary'),
6882
- padding: '6px 12px',
8016
+ /**
8017
+ * The panel anchors to the detached `anchor` when one resolves (id strings
8018
+ * are looked up now, at open time), else to the trigger span.
8019
+ */
8020
+ applyAnchor() {
8021
+ const el = resolveElement(this.anchor()) ?? this.triggerRef().nativeElement;
8022
+ if (this.anchoredEl === el)
8023
+ return;
8024
+ if (this.anchoredEl)
8025
+ clearAnchorName(this.anchoredEl);
8026
+ setAnchorName(el, this.anchorName);
8027
+ this.anchoredEl = el;
8028
+ }
8029
+ onToggle(event) {
8030
+ const isOpen = isToggleOpen(event);
8031
+ this.showing.set(isOpen);
8032
+ this.open.set(isOpen);
8033
+ const panel = this.panelRef().nativeElement;
8034
+ if (isOpen) {
8035
+ if (this.mode() === 'rich') {
8036
+ panel.querySelector('[autofocus]')?.focus();
8037
+ }
8038
+ else {
8039
+ this.attachTooltipEscape();
8040
+ if (typeof ngDevMode !== 'undefined' && ngDevMode && panel.querySelector(FOCUSABLE_SELECTOR)) {
8041
+ console.warn('[uni-popover] tooltip-mode content must not contain focusable elements — ' +
8042
+ 'a tooltip is a description, not a surface. Use mode="rich" instead.');
8043
+ }
8044
+ }
8045
+ this.opened.emit();
8046
+ }
8047
+ else {
8048
+ this.detachEscape?.();
8049
+ if (this.mode() === 'rich' && this.hasTrigger()) {
8050
+ restoreOverlayFocus(panel, resolveFocusTarget(this.triggerRef().nativeElement));
8051
+ }
8052
+ this.closed.emit();
8053
+ }
8054
+ }
8055
+ closeFromButton() {
8056
+ this.hidePopover();
8057
+ if (this.hasTrigger())
8058
+ resolveFocusTarget(this.triggerRef().nativeElement).focus();
8059
+ }
8060
+ // --- rich-mode gesture ----------------------------------------------------
8061
+ onTriggerClick(event) {
8062
+ if (this.mode() !== 'rich')
8063
+ return;
8064
+ this.togglePopover(event);
8065
+ }
8066
+ // --- tooltip-mode gestures (WCAG 1.4.13) ----------------------------------
8067
+ openDelayMs = computed(() => this.openDelay() ?? this.componentOptions().tooltipOpenDelay, ...(ngDevMode ? [{ debugName: "openDelayMs" }] : /* istanbul ignore next */ []));
8068
+ closeDelayMs = computed(() => this.closeDelay() ?? this.componentOptions().tooltipCloseDelay, ...(ngDevMode ? [{ debugName: "closeDelayMs" }] : /* istanbul ignore next */ []));
8069
+ onTriggerEnter() {
8070
+ if (this.mode() !== 'tooltip')
8071
+ return;
8072
+ this.closeTimer.stop();
8073
+ this.openTimer.start(this.openDelayMs(), () => this.showPopover());
8074
+ }
8075
+ onTriggerLeave() {
8076
+ if (this.mode() !== 'tooltip')
8077
+ return;
8078
+ this.openTimer.stop();
8079
+ if (this.showing())
8080
+ this.closeTimer.start(this.closeDelayMs(), () => this.hidePopover());
8081
+ }
8082
+ onTriggerFocusIn() {
8083
+ if (this.mode() !== 'tooltip')
8084
+ return;
8085
+ this.closeTimer.stop();
8086
+ this.showPopover();
8087
+ }
8088
+ onTriggerFocusOut() {
8089
+ if (this.mode() !== 'tooltip')
8090
+ return;
8091
+ this.openTimer.stop();
8092
+ this.closeTimer.stop();
8093
+ this.hidePopover();
8094
+ }
8095
+ /** Resting the pointer inside the panel must not dismiss it (hoverable). */
8096
+ onPanelEnter() {
8097
+ if (this.mode() !== 'tooltip')
8098
+ return;
8099
+ this.closeTimer.stop();
8100
+ }
8101
+ onPanelLeave() {
8102
+ if (this.mode() !== 'tooltip')
8103
+ return;
8104
+ if (this.showing())
8105
+ this.closeTimer.start(this.closeDelayMs(), () => this.hidePopover());
8106
+ }
8107
+ /**
8108
+ * Tooltip panels are `manual`, so Escape is ours: dismiss without moving
8109
+ * focus, and stop propagation so an enclosing dialog stays open. Capture
8110
+ * phase, document-level — the pointer may be nowhere near the trigger.
8111
+ */
8112
+ attachTooltipEscape() {
8113
+ const onKeydown = (event) => {
8114
+ if (event.key !== 'Escape')
8115
+ return;
8116
+ event.stopPropagation();
8117
+ this.openTimer.stop();
8118
+ this.closeTimer.stop();
8119
+ this.hidePopover();
8120
+ };
8121
+ document.addEventListener('keydown', onKeydown, true);
8122
+ this.detachEscape = () => {
8123
+ document.removeEventListener('keydown', onKeydown, true);
8124
+ this.detachEscape = null;
8125
+ };
8126
+ }
8127
+ // --- styling --------------------------------------------------------------
8128
+ triggerClassName = css({ display: 'inline-block' });
8129
+ resolvedMaxWidth = computed(() => {
8130
+ const width = this.maxWidth();
8131
+ if (width === undefined || width === '')
8132
+ return this.componentOptions().maxWidth;
8133
+ return typeof width === 'number' ? `${width}px` : width;
8134
+ }, ...(ngDevMode ? [{ debugName: "resolvedMaxWidth" }] : /* istanbul ignore next */ []));
8135
+ popoverClassName = computed(() => {
8136
+ const options = this.componentOptions();
8137
+ return css({
8138
+ ...this.theme.colorPair(options.color),
8139
+ ...this.theme.radius(options.borderRadius),
8140
+ ...this.theme.boxShadow(options.shadow),
8141
+ ...this.theme.typeface(options.typeface),
8142
+ ...this.theme.border(options.border),
8143
+ padding: 0,
6883
8144
  width: 'max-content',
8145
+ maxWidth: this.resolvedMaxWidth(),
6884
8146
  overflow: 'visible',
6885
- ...anchorStyles(this.anchorName, this.placement(), { mainAxis: 7 }),
6886
- // Fade via discrete transitions across the top layer
6887
- transitionProperty: 'opacity, display, overlay',
6888
- transitionDuration: '250ms',
6889
- transitionBehavior: 'allow-discrete',
6890
- opacity: 0,
6891
- '&:popover-open': {
6892
- opacity: 1,
6893
- },
6894
- '@starting-style': {
6895
- '&:popover-open': {
6896
- opacity: 0,
6897
- },
6898
- },
6899
- },
6900
- ]), ...(ngDevMode ? [{ debugName: "popoverClassName" }] : /* istanbul ignore next */ []));
8147
+ ...anchorStyles(this.anchorName, this.placement(), { mainAxis: options.offset }),
8148
+ ...discreteOverlayTransition(250, { opacity: 0 }, { opacity: 1 }),
8149
+ });
8150
+ }, ...(ngDevMode ? [{ debugName: "popoverClassName" }] : /* istanbul ignore next */ []));
8151
+ /** Empty regions collapse, so bare content renders v1's single-region look. */
8152
+ headerRowClassName = computed(() => {
8153
+ const options = this.componentOptions();
8154
+ return css({
8155
+ display: 'flex',
8156
+ alignItems: 'center',
8157
+ gap: 8,
8158
+ padding: options.padding,
8159
+ paddingBottom: 0,
8160
+ '&:not(:has(*))': { display: 'none' },
8161
+ });
8162
+ }, ...(ngDevMode ? [{ debugName: "headerRowClassName" }] : /* istanbul ignore next */ []));
8163
+ titleClassName = computed(() => css({ ...this.theme.typeface(this.componentOptions().headerTypeface), marginRight: 'auto' }), ...(ngDevMode ? [{ debugName: "titleClassName" }] : /* istanbul ignore next */ []));
8164
+ bodyClassName = computed(() => {
8165
+ const options = this.componentOptions();
8166
+ return css({
8167
+ padding: this.mode() === 'tooltip' ? options.tooltipPadding : options.padding,
8168
+ '&:empty': { display: 'none' },
8169
+ });
8170
+ }, ...(ngDevMode ? [{ debugName: "bodyClassName" }] : /* istanbul ignore next */ []));
8171
+ footerRowClassName = computed(() => {
8172
+ const options = this.componentOptions();
8173
+ return css({
8174
+ display: 'flex',
8175
+ alignItems: 'center',
8176
+ justifyContent: 'flex-end',
8177
+ gap: 8,
8178
+ padding: options.padding,
8179
+ paddingTop: 0,
8180
+ '&:not(:has(*))': { display: 'none' },
8181
+ });
8182
+ }, ...(ngDevMode ? [{ debugName: "footerRowClassName" }] : /* istanbul ignore next */ []));
6901
8183
  arrowClassName = computed(() => {
8184
+ const options = this.componentOptions();
6902
8185
  const side = this.placement().split('-')[0];
6903
- // Border on the two edges of the rotated square that face outward
8186
+ // Border on the two edges of the clipped half that face outward
6904
8187
  const borders = {
6905
- top: { ...this.theme.borderRight('quaternary'), ...this.theme.borderBottom('quaternary') },
6906
- bottom: { ...this.theme.borderLeft('quaternary'), ...this.theme.borderTop('quaternary') },
6907
- left: { ...this.theme.borderRight('quaternary'), ...this.theme.borderTop('quaternary') },
6908
- right: { ...this.theme.borderLeft('quaternary'), ...this.theme.borderBottom('quaternary') },
8188
+ top: { ...this.theme.borderRight(options.border), ...this.theme.borderBottom(options.border) },
8189
+ bottom: { ...this.theme.borderLeft(options.border), ...this.theme.borderTop(options.border) },
8190
+ left: { ...this.theme.borderRight(options.border), ...this.theme.borderTop(options.border) },
8191
+ right: { ...this.theme.borderLeft(options.border), ...this.theme.borderBottom(options.border) },
6909
8192
  };
6910
8193
  return css({
6911
- ...this.theme.colorPair('primary-surface'),
6912
- ...anchorArrowStyles(this.placement()),
8194
+ ...this.theme.colorPair(options.color),
8195
+ ...anchorArrowStyles(this.placement(), options.arrowSize),
6913
8196
  ...borders[side],
6914
8197
  });
6915
8198
  }, ...(ngDevMode ? [{ debugName: "arrowClassName" }] : /* istanbul ignore next */ []));
6916
8199
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniPopoverComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6917
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.12", type: UniPopoverComponent, isStandalone: true, selector: "uni-popover", inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, autoClose: { classPropertyName: "autoClose", publicName: "autoClose", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "panelRef", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Click bubbles up from the projected [trigger] content, which is expected\n to be focusable (a button); keyboard activation arrives as a click. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<span #trigger (click)=\"togglePopover($event)\" [class]=\"triggerClassName\">\n <ng-content select=\"[trigger]\"></ng-content>\n</span>\n<div\n #panel\n [id]=\"panelId\"\n [attr.popover]=\"autoClose() ? 'auto' : 'manual'\"\n [class]=\"popoverClassName()\"\n (toggle)=\"onToggle($event)\"\n>\n <ng-content></ng-content>\n <span [class]=\"arrowClassName()\"></span>\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
8200
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniPopoverComponent, isStandalone: true, selector: "uni-popover", inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, autoClose: { classPropertyName: "autoClose", publicName: "autoClose", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, anchor: { classPropertyName: "anchor", publicName: "anchor", isSignal: true, isRequired: false, transformFunction: null }, header: { classPropertyName: "header", publicName: "header", isSignal: true, isRequired: false, transformFunction: null }, closable: { classPropertyName: "closable", publicName: "closable", isSignal: true, isRequired: false, transformFunction: null }, arrow: { classPropertyName: "arrow", publicName: "arrow", isSignal: true, isRequired: false, transformFunction: null }, maxWidth: { classPropertyName: "maxWidth", publicName: "maxWidth", isSignal: true, isRequired: false, transformFunction: null }, openDelay: { classPropertyName: "openDelay", publicName: "openDelay", isSignal: true, isRequired: false, transformFunction: null }, closeDelay: { classPropertyName: "closeDelay", publicName: "closeDelay", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", opened: "opened", closed: "closed" }, providers: [{ provide: COMPONENT_NAME, useValue: 'popover' }], viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "panelRef", first: true, predicate: ["panel"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Click bubbles up from the projected [trigger] content, which is expected\n to be focusable (a button); keyboard activation arrives as a click. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<span\n #trigger\n [class]=\"triggerClassName\"\n (click)=\"onTriggerClick($event)\"\n (mouseenter)=\"onTriggerEnter()\"\n (mouseleave)=\"onTriggerLeave()\"\n (focusin)=\"onTriggerFocusIn()\"\n (focusout)=\"onTriggerFocusOut()\"\n>\n <ng-content select=\"[trigger]\"></ng-content>\n</span>\n<div\n #panel\n [id]=\"panelId\"\n [attr.popover]=\"popoverKind()\"\n [attr.role]=\"mode() === 'tooltip' ? 'tooltip' : null\"\n [attr.aria-labelledby]=\"header() ? headerId : null\"\n [class]=\"popoverClassName()\"\n (toggle)=\"onToggle($event)\"\n (mouseenter)=\"onPanelEnter()\"\n (mouseleave)=\"onPanelLeave()\"\n>\n <div [class]=\"headerRowClassName()\">\n <ng-content select=\"[popover-header]\"></ng-content>\n @if (header()) {\n <span [id]=\"headerId\" [class]=\"titleClassName()\">{{ header() }}</span>\n }\n @if (closable()) {\n <button\n icon-button\n [iconName]=\"componentOptions().closeSymbol\"\n variant=\"ghost\"\n size=\"sm\"\n (click)=\"closeFromButton()\"\n >\n Close\n </button>\n }\n </div>\n <div [class]=\"bodyClassName()\"><ng-content></ng-content></div>\n <div [class]=\"footerRowClassName()\"><ng-content select=\"[popover-footer]\"></ng-content></div>\n @if (arrow()) {\n <span [class]=\"arrowClassName()\"></span>\n }\n</div>\n", dependencies: [{ kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6918
8201
  }
6919
8202
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniPopoverComponent, decorators: [{
6920
8203
  type: Component,
6921
- args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-popover', imports: [], template: "<!-- Click bubbles up from the projected [trigger] content, which is expected\n to be focusable (a button); keyboard activation arrives as a click. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<span #trigger (click)=\"togglePopover($event)\" [class]=\"triggerClassName\">\n <ng-content select=\"[trigger]\"></ng-content>\n</span>\n<div\n #panel\n [id]=\"panelId\"\n [attr.popover]=\"autoClose() ? 'auto' : 'manual'\"\n [class]=\"popoverClassName()\"\n (toggle)=\"onToggle($event)\"\n>\n <ng-content></ng-content>\n <span [class]=\"arrowClassName()\"></span>\n</div>\n" }]
6922
- }], ctorParameters: () => [], propDecorators: { placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], autoClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoClose", required: false }] }], triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], panelRef: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }] } });
8204
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-popover', imports: [UniIconButtonComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'popover' }], template: "<!-- Click bubbles up from the projected [trigger] content, which is expected\n to be focusable (a button); keyboard activation arrives as a click. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<span\n #trigger\n [class]=\"triggerClassName\"\n (click)=\"onTriggerClick($event)\"\n (mouseenter)=\"onTriggerEnter()\"\n (mouseleave)=\"onTriggerLeave()\"\n (focusin)=\"onTriggerFocusIn()\"\n (focusout)=\"onTriggerFocusOut()\"\n>\n <ng-content select=\"[trigger]\"></ng-content>\n</span>\n<div\n #panel\n [id]=\"panelId\"\n [attr.popover]=\"popoverKind()\"\n [attr.role]=\"mode() === 'tooltip' ? 'tooltip' : null\"\n [attr.aria-labelledby]=\"header() ? headerId : null\"\n [class]=\"popoverClassName()\"\n (toggle)=\"onToggle($event)\"\n (mouseenter)=\"onPanelEnter()\"\n (mouseleave)=\"onPanelLeave()\"\n>\n <div [class]=\"headerRowClassName()\">\n <ng-content select=\"[popover-header]\"></ng-content>\n @if (header()) {\n <span [id]=\"headerId\" [class]=\"titleClassName()\">{{ header() }}</span>\n }\n @if (closable()) {\n <button\n icon-button\n [iconName]=\"componentOptions().closeSymbol\"\n variant=\"ghost\"\n size=\"sm\"\n (click)=\"closeFromButton()\"\n >\n Close\n </button>\n }\n </div>\n <div [class]=\"bodyClassName()\"><ng-content></ng-content></div>\n <div [class]=\"footerRowClassName()\"><ng-content select=\"[popover-footer]\"></ng-content></div>\n @if (arrow()) {\n <span [class]=\"arrowClassName()\"></span>\n }\n</div>\n" }]
8205
+ }], ctorParameters: () => [], propDecorators: { placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], autoClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoClose", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], anchor: [{ type: i0.Input, args: [{ isSignal: true, alias: "anchor", required: false }] }], header: [{ type: i0.Input, args: [{ isSignal: true, alias: "header", required: false }] }], closable: [{ type: i0.Input, args: [{ isSignal: true, alias: "closable", required: false }] }], arrow: [{ type: i0.Input, args: [{ isSignal: true, alias: "arrow", required: false }] }], maxWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxWidth", required: false }] }], openDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "openDelay", required: false }] }], closeDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeDelay", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], panelRef: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }] } });
6923
8206
 
6924
8207
  /**
6925
8208
  * UniPopoverComponent Barrel File
@@ -7236,35 +8519,7 @@ class UniSearchInputComponent extends BaseComponent {
7236
8519
  ...this.theme.paddingLeft('sm'),
7237
8520
  },
7238
8521
  }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
7239
- listClass = computed(() => {
7240
- const options = this.componentOptions();
7241
- return css({
7242
- position: 'absolute',
7243
- top: '100%',
7244
- left: 0,
7245
- right: 0,
7246
- zIndex: 20,
7247
- margin: '4px 0 0',
7248
- padding: 4,
7249
- listStyle: 'none',
7250
- maxHeight: 280,
7251
- overflowY: 'auto',
7252
- ...this.theme.backgroundColor(options.listColor ?? 'primary-surface'),
7253
- ...this.theme.boxShadow(options.listShadow ?? 'menu'),
7254
- ...this.theme.radius(options.listBorderRadius ?? 'xs'),
7255
- '& [role="option"]': {
7256
- padding: '8px 12px',
7257
- cursor: 'pointer',
7258
- ...this.theme.typeface('label'),
7259
- ...this.theme.color('on-primary-surface'),
7260
- ...this.theme.radius('xxs'),
7261
- '&.active, &:hover': {
7262
- ...this.theme.backgroundColor('primary-container'),
7263
- ...this.theme.color('on-primary-container'),
7264
- },
7265
- },
7266
- });
7267
- }, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
8522
+ listClass = computed(() => css(listboxPopupStyles(this.theme, this.componentOptions())), ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
7268
8523
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSearchInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
7269
8524
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniSearchInputComponent, isStandalone: true, selector: "uni-search-input", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { change: "change", search: "search", suggestionSelected: "suggestionSelected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'searchInput' }], viewQueries: [{ propertyName: "field", first: true, predicate: UniDebounceInputComponent, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Keydown/focusout ride the bubbling events from the inner input; the\n wrapper itself stays out of the tab order. -->\n<!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n<div (keydown)=\"onKeydown($event)\" (focusout)=\"onFocusOut($event)\">\n <uni-debounce-input\n #searchField\n [label]=\"label()\"\n [placeholder]=\"placeholder() ?? label()\"\n [debounceTime]=\"debounceTime()\"\n role=\"combobox\"\n [ariaExpanded]=\"list.open()\"\n [ariaControls]=\"listboxId\"\n [ariaActivedescendant]=\"list.activeDescendantId() ?? undefined\"\n (change)=\"handleChange($event)\"\n >\n <uni-symbol pre-input class=\"uni-search-lead\" name=\"{{ componentOptions().searchSymbol ?? 'search' }}\" aria-hidden=\"true\" />\n @if (hasQuery()) {\n <button\n post-input\n icon-button\n [iconName]=\"componentOptions().clearSymbol ?? 'close'\"\n variant=\"ghost\"\n size=\"sm\"\n (click)=\"clear()\"\n >\n Clear search\n </button>\n }\n </uni-debounce-input>\n\n @if (list.open()) {\n <ul [id]=\"listboxId\" role=\"listbox\" [attr.aria-label]=\"label()\" [class]=\"listClass()\">\n @for (suggestion of visibleSuggestions(); track suggestion; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"i === list.activeIndex()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"select(suggestion)\"\n >\n {{ suggestion }}\n </li>\n }\n </ul>\n }\n</div>\n", dependencies: [{ kind: "component", type: UniDebounceInputComponent, selector: "uni-debounce-input", inputs: ["inputName", "inputId", "debounceTime", "label", "placeholder", "disabled", "role", "ariaExpanded", "ariaControls", "ariaActivedescendant"], outputs: ["change"] }, { kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7270
8525
  }
@@ -8534,35 +9789,7 @@ class UniTagInputComponent extends BaseComponent {
8534
9789
  font: 'inherit',
8535
9790
  padding: 0,
8536
9791
  }), ...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
8537
- listClass = computed(() => {
8538
- const options = this.componentOptions();
8539
- return css({
8540
- position: 'absolute',
8541
- top: '100%',
8542
- left: 0,
8543
- right: 0,
8544
- zIndex: 20,
8545
- margin: '4px 0 0',
8546
- padding: 4,
8547
- listStyle: 'none',
8548
- maxHeight: 280,
8549
- overflowY: 'auto',
8550
- ...this.theme.backgroundColor(options.listColor ?? 'primary-surface'),
8551
- ...this.theme.boxShadow(options.listShadow ?? 'menu'),
8552
- ...this.theme.radius(options.listBorderRadius ?? 'xs'),
8553
- '& [role="option"]': {
8554
- padding: '8px 12px',
8555
- cursor: 'pointer',
8556
- ...this.theme.typeface('label'),
8557
- ...this.theme.color('on-primary-surface'),
8558
- ...this.theme.radius('xxs'),
8559
- '&.active, &:hover': {
8560
- ...this.theme.backgroundColor('primary-container'),
8561
- ...this.theme.color('on-primary-container'),
8562
- },
8563
- },
8564
- });
8565
- }, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
9792
+ listClass = computed(() => css(listboxPopupStyles(this.theme, this.componentOptions())), ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
8566
9793
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTagInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
8567
9794
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniTagInputComponent, isStandalone: true, selector: "uni-tag-input", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, preset: { classPropertyName: "preset", publicName: "preset", isSignal: true, isRequired: false, transformFunction: null }, separators: { classPropertyName: "separators", publicName: "separators", isSignal: true, isRequired: false, transformFunction: null }, commitOnBlur: { classPropertyName: "commitOnBlur", publicName: "commitOnBlur", isSignal: true, isRequired: false, transformFunction: null }, allowDuplicates: { classPropertyName: "allowDuplicates", publicName: "allowDuplicates", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, validate: { classPropertyName: "validate", publicName: "validate", isSignal: true, isRequired: false, transformFunction: null }, parse: { classPropertyName: "parse", publicName: "parse", isSignal: true, isRequired: false, transformFunction: null }, tagVariant: { classPropertyName: "tagVariant", publicName: "tagVariant", isSignal: true, isRequired: false, transformFunction: null }, tagTone: { classPropertyName: "tagTone", publicName: "tagTone", isSignal: true, isRequired: false, transformFunction: null }, tagSize: { classPropertyName: "tagSize", publicName: "tagSize", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", query: "query", added: "added", removed: "removed", rejected: "rejected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'tagInput' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["field"], descendants: true, isSignal: true }, { propertyName: "chipRefs", predicate: ["chip"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Focusout rides the bubbling event so the list closes only when focus\n leaves the whole field, not while moving between chips and the input. -->\n<div style=\"position: relative\" (focusout)=\"onFocusOut($event)\">\n <uni-input-box [error]=\"showError()\" height=\"auto\">\n <ul [class]=\"fieldClass()\">\n @for (item of value(); track item.value; let i = $index) {\n <!-- The focusable element is the chip's own button; this <li> only\n receives keys bubbling up from it, so it must stay out of the tab\n order (same delegation the menu uses for its roving focus). -->\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <li #chip (keydown)=\"onChipKeydown($event, i)\" (dblclick)=\"editChip(i)\">\n <uni-tag\n [label]=\"labelOf(item)\"\n [value]=\"item.value\"\n [avatarSrc]=\"item.avatarSrc\"\n [variant]=\"item.invalid ? 'warn' : tagVariant()\"\n [tone]=\"tagTone()\"\n [size]=\"tagSize() ?? componentOptions().chipSize ?? 'md'\"\n [invalid]=\"!!item.invalid\"\n [disabled]=\"!!item.disabled || disabled()\"\n [interactive]=\"true\"\n [removable]=\"!item.disabled && !disabled()\"\n [controlTabIndex]=\"-1\"\n (removed)=\"removeAt(i)\"\n />\n </li>\n }\n\n <li [class]=\"inputClass()\">\n <!-- The field is one tab stop: chips carry tabindex=\"-1\" and are\n entered with Backspace/ArrowLeft, so Tab never walks through\n eight recipients to reach the next control. -->\n <input\n #field\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder() ?? ''\"\n [attr.aria-label]=\"label()\"\n [attr.aria-expanded]=\"list.open()\"\n [attr.aria-controls]=\"list.listboxId\"\n [attr.aria-activedescendant]=\"list.activeDescendantId()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput($any($event.target).value)\"\n (paste)=\"onPaste($event)\"\n (blur)=\"onBlur()\"\n />\n </li>\n </ul>\n </uni-input-box>\n\n @if (list.open()) {\n <ul [id]=\"list.listboxId\" role=\"listbox\" [attr.aria-label]=\"label()\" [class]=\"listClass()\">\n @for (suggestion of visibleSuggestions(); track suggestion.value; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"i === list.activeIndex()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"selectSuggestion(suggestion)\"\n >\n {{ labelOf(suggestion) }}\n @if (suggestion.description) {\n <span [class]=\"srOnly\">{{ suggestion.description }}</span>\n }\n </li>\n }\n </ul>\n }\n\n <!-- Said once for the whole field, rather than every chip advertising it. -->\n <span [id]=\"hintId\" [class]=\"srOnly\">\n Press Backspace to reach the last entry, then Backspace again to remove it.\n </span>\n\n <!-- Adds and removes are otherwise silent for a screen reader. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n</div>\n", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }, { kind: "component", type: UniTagComponent, selector: "uni-tag", inputs: ["size", "tone", "label", "value", "maxWidth", "avatarSrc", "avatarName", "iconName", "symbolName", "dot", "removable", "interactive", "selected", "invalid", "disabled", "removeLabel", "controlTabIndex"], outputs: ["removed", "activated"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8568
9795
  }
@@ -9836,9 +11063,221 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
9836
11063
  }]
9837
11064
  }], ctorParameters: () => [], propDecorators: { hoverDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoverDelay", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], inlineText: [{ type: i0.Input, args: [{ isSignal: true, alias: "inlineText", required: false }] }], appendToBody: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendToBody", required: false }] }], tipRef: [{ type: i0.ViewChild, args: ['tip', { isSignal: true }] }] } });
9838
11065
 
11066
+ /**
11067
+ * A thin sequencer over one `uni-callout`: steps spotlight their targets, a
11068
+ * footer walks Next/Back with dots-or-fraction progress, and `advanceOn`
11069
+ * gates a step behind an interaction with its target (auto-advancing for
11070
+ * clicks, unlocking Next otherwise — announced through one `role="status"`
11071
+ * region). Escape or the close button skips the tour and reports the step.
11072
+ *
11073
+ * `active` is a two-way model, so a tour is deep-linkable and inspectable;
11074
+ * steps whose declared target cannot be resolved are skipped (with a dev
11075
+ * warning) in the direction of travel rather than erroring.
11076
+ */
11077
+ class UniTourComponent extends BaseComponent {
11078
+ renderer = inject(Renderer2);
11079
+ destroyRef = inject(DestroyRef);
11080
+ steps = input.required(...(ngDevMode ? [{ debugName: "steps" }] : /* istanbul ignore next */ []));
11081
+ /** The presented step index, or null when the tour is not running. */
11082
+ active = model(null, ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
11083
+ nextLabel = input('Next', ...(ngDevMode ? [{ debugName: "nextLabel" }] : /* istanbul ignore next */ []));
11084
+ backLabel = input('Back', ...(ngDevMode ? [{ debugName: "backLabel" }] : /* istanbul ignore next */ []));
11085
+ skipLabel = input('Skip', ...(ngDevMode ? [{ debugName: "skipLabel" }] : /* istanbul ignore next */ []));
11086
+ doneLabel = input('Done', ...(ngDevMode ? [{ debugName: "doneLabel" }] : /* istanbul ignore next */ []));
11087
+ started = output();
11088
+ stepChanged = output();
11089
+ finished = output();
11090
+ skipped = output();
11091
+ calloutOpen = signal(false, ...(ngDevMode ? [{ debugName: "calloutOpen" }] : /* istanbul ignore next */ []));
11092
+ satisfied = signal(true, ...(ngDevMode ? [{ debugName: "satisfied" }] : /* istanbul ignore next */ []));
11093
+ announcement = signal('', ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
11094
+ resolvedTarget = signal(undefined, ...(ngDevMode ? [{ debugName: "resolvedTarget" }] : /* istanbul ignore next */ []));
11095
+ presentedIndex = signal(null, ...(ngDevMode ? [{ debugName: "presentedIndex" }] : /* istanbul ignore next */ []));
11096
+ gateCleanup = null;
11097
+ index = computed(() => this.presentedIndex() ?? 0, ...(ngDevMode ? [{ debugName: "index" }] : /* istanbul ignore next */ []));
11098
+ currentStep = computed(() => {
11099
+ const index = this.presentedIndex();
11100
+ return index === null ? undefined : this.steps()[index];
11101
+ }, ...(ngDevMode ? [{ debugName: "currentStep" }] : /* istanbul ignore next */ []));
11102
+ isLast = computed(() => this.index() >= this.steps().length - 1, ...(ngDevMode ? [{ debugName: "isLast" }] : /* istanbul ignore next */ []));
11103
+ clickGate = computed(() => this.currentStep()?.advanceOn?.event === 'click', ...(ngDevMode ? [{ debugName: "clickGate" }] : /* istanbul ignore next */ []));
11104
+ /** Gated steps force an interactive target — the gate needs the gesture. */
11105
+ targetInteractive = computed(() => {
11106
+ const step = this.currentStep();
11107
+ return step?.advanceOn ? true : (step?.targetInteractive ?? true);
11108
+ }, ...(ngDevMode ? [{ debugName: "targetInteractive" }] : /* istanbul ignore next */ []));
11109
+ stepAriaLabel = computed(() => {
11110
+ const step = this.currentStep();
11111
+ return step ? `${step.title}, step ${this.index() + 1} of ${this.steps().length}` : '';
11112
+ }, ...(ngDevMode ? [{ debugName: "stepAriaLabel" }] : /* istanbul ignore next */ []));
11113
+ constructor() {
11114
+ super();
11115
+ // External writes to `active` (deep links) route into the sequencer.
11116
+ effect(() => {
11117
+ const index = this.active();
11118
+ if (index === untracked(this.presentedIndex))
11119
+ return;
11120
+ untracked(() => (index === null ? this.stopTour() : this.present(index, 1)));
11121
+ });
11122
+ this.destroyRef.onDestroy(() => this.gateCleanup?.());
11123
+ }
11124
+ start(at = 0) {
11125
+ this.started.emit();
11126
+ this.present(at, 1);
11127
+ }
11128
+ next() {
11129
+ this.advance();
11130
+ }
11131
+ back() {
11132
+ const index = this.presentedIndex();
11133
+ if (index !== null && index > 0)
11134
+ this.present(index - 1, -1);
11135
+ }
11136
+ skip() {
11137
+ const step = this.currentStep();
11138
+ const index = this.presentedIndex();
11139
+ this.stopTour();
11140
+ if (step && index !== null)
11141
+ this.skipped.emit({ key: step.key, index });
11142
+ }
11143
+ advance() {
11144
+ const index = this.presentedIndex();
11145
+ if (index === null)
11146
+ return;
11147
+ if (index >= this.steps().length - 1)
11148
+ this.finishTour();
11149
+ else
11150
+ this.present(index + 1, 1);
11151
+ }
11152
+ /**
11153
+ * Present step `index`, skipping (in the direction of travel) steps whose
11154
+ * declared target cannot be resolved right now.
11155
+ */
11156
+ present(index, dir) {
11157
+ this.gateCleanup?.();
11158
+ const steps = this.steps();
11159
+ if (index < 0 || index >= steps.length) {
11160
+ this.finishTour();
11161
+ return;
11162
+ }
11163
+ const step = steps[index];
11164
+ let target = null;
11165
+ if (step.target !== undefined && step.target !== '') {
11166
+ target = resolveElement(step.target);
11167
+ if (!target) {
11168
+ if (typeof ngDevMode === 'undefined' || ngDevMode) {
11169
+ console.warn(`[uni-tour] step "${step.key}" target missing — skipped`);
11170
+ }
11171
+ this.present(index + dir, dir);
11172
+ return;
11173
+ }
11174
+ }
11175
+ this.presentedIndex.set(index);
11176
+ this.active.set(index);
11177
+ this.resolvedTarget.set(target ?? undefined);
11178
+ this.setupGate(step, target);
11179
+ this.calloutOpen.set(true);
11180
+ this.stepChanged.emit({ key: step.key, index });
11181
+ }
11182
+ setupGate(step, target) {
11183
+ const gate = step.advanceOn;
11184
+ this.satisfied.set(!gate);
11185
+ if (!gate || !target)
11186
+ return;
11187
+ const auto = gate.auto ?? gate.event === 'click';
11188
+ const unlisten = this.renderer.listen(target, gate.event, () => {
11189
+ if (untracked(this.satisfied) && !auto)
11190
+ return;
11191
+ this.satisfied.set(true);
11192
+ if (auto) {
11193
+ this.gateCleanup?.();
11194
+ this.advance();
11195
+ }
11196
+ else {
11197
+ this.announcement.set('Next available');
11198
+ }
11199
+ });
11200
+ this.gateCleanup = () => {
11201
+ unlisten();
11202
+ this.gateCleanup = null;
11203
+ };
11204
+ }
11205
+ onDismissed(dismissal) {
11206
+ if (dismissal.reason === 'programmatic')
11207
+ return; // that's us, stepping or stopping
11208
+ const step = this.currentStep();
11209
+ const index = this.presentedIndex();
11210
+ this.stopTour();
11211
+ if (step && index !== null)
11212
+ this.skipped.emit({ key: step.key, index });
11213
+ }
11214
+ /** Arrow keys navigate only while focus is inside the panel. */
11215
+ onPanelKeydown(event) {
11216
+ if (event.key === 'ArrowRight' && this.satisfied() && !this.clickGate()) {
11217
+ event.preventDefault();
11218
+ this.advance();
11219
+ }
11220
+ else if (event.key === 'ArrowLeft' && this.index() > 0) {
11221
+ event.preventDefault();
11222
+ this.back();
11223
+ }
11224
+ }
11225
+ finishTour() {
11226
+ this.gateCleanup?.();
11227
+ this.calloutOpen.set(false);
11228
+ this.presentedIndex.set(null);
11229
+ this.active.set(null);
11230
+ this.finished.emit();
11231
+ }
11232
+ stopTour() {
11233
+ this.gateCleanup?.();
11234
+ this.calloutOpen.set(false);
11235
+ this.presentedIndex.set(null);
11236
+ this.active.set(null);
11237
+ }
11238
+ // --- styling --------------------------------------------------------------
11239
+ statusClassName = css(visuallyHidden);
11240
+ footerClassName = computed(() => css({
11241
+ display: 'flex',
11242
+ alignItems: 'center',
11243
+ flex: 1,
11244
+ gap: this.theme.spacing()[this.componentOptions().footerGap],
11245
+ }), ...(ngDevMode ? [{ debugName: "footerClassName" }] : /* istanbul ignore next */ []));
11246
+ dotsClassName = computed(() => {
11247
+ const colors = this.theme.colorPalette();
11248
+ const on = colors[this.variant()] ?? colors['primary'];
11249
+ return css({
11250
+ display: 'inline-flex',
11251
+ gap: 5,
11252
+ margin: '0 auto',
11253
+ '& i': {
11254
+ width: 6,
11255
+ height: 6,
11256
+ borderRadius: 999,
11257
+ background: colors['surface-variant'],
11258
+ border: `1px solid ${colors['outline'] ?? colors['surface-variant']}`,
11259
+ },
11260
+ '& i.on': { background: on, borderColor: on },
11261
+ });
11262
+ }, ...(ngDevMode ? [{ debugName: "dotsClassName" }] : /* istanbul ignore next */ []));
11263
+ fractionClassName = css({ margin: '0 auto' });
11264
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTourComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11265
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniTourComponent, isStandalone: true, selector: "uni-tour", inputs: { steps: { classPropertyName: "steps", publicName: "steps", isSignal: true, isRequired: true, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null }, backLabel: { classPropertyName: "backLabel", publicName: "backLabel", isSignal: true, isRequired: false, transformFunction: null }, skipLabel: { classPropertyName: "skipLabel", publicName: "skipLabel", isSignal: true, isRequired: false, transformFunction: null }, doneLabel: { classPropertyName: "doneLabel", publicName: "doneLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { active: "activeChange", started: "started", stepChanged: "stepChanged", finished: "finished", skipped: "skipped" }, providers: [{ provide: COMPONENT_NAME, useValue: 'tour' }], usesInheritance: true, ngImport: i0, template: "<uni-callout\n [(open)]=\"calloutOpen\"\n [key]=\"currentStep()?.key ?? ''\"\n [target]=\"resolvedTarget()\"\n [placement]=\"currentStep()?.placement ?? 'bottom'\"\n [backdrop]=\"currentStep()?.backdrop\"\n [targetInteractive]=\"targetInteractive()\"\n [header]=\"currentStep()?.title ?? ''\"\n [ariaLabel]=\"stepAriaLabel()\"\n [closeLabel]=\"skipLabel()\"\n (dismissed)=\"onDismissed($event)\"\n (panelKeydown)=\"onPanelKeydown($event)\"\n>\n <p>{{ currentStep()?.body }}</p>\n <div callout-actions [class]=\"footerClassName()\">\n @if (index() > 0) {\n <button text-button variant=\"ghost\" (click)=\"back()\">{{ backLabel() }}</button>\n }\n @if (componentOptions().progressStyle === 'fraction') {\n <span aria-hidden=\"true\" [class]=\"fractionClassName\">{{ index() + 1 }} of {{ steps().length }}</span>\n } @else {\n <span aria-hidden=\"true\" [class]=\"dotsClassName()\">\n @for (step of steps(); track $index) {\n <i [class.on]=\"$index === index()\"></i>\n }\n </span>\n }\n <!-- A click-gate advances by using the target itself \u2014 a Next button\n would be a lie, so none renders. -->\n @if (!clickGate()) {\n <button text-button [disable]=\"!satisfied()\" (click)=\"advance()\">\n {{ isLast() ? doneLabel() : nextLabel() }}\n </button>\n }\n </div>\n</uni-callout>\n<!-- One live region per tour: gate unlocks are announced here. -->\n<span role=\"status\" [class]=\"statusClassName\">{{ announcement() }}</span>\n", dependencies: [{ kind: "component", type: UniButtonComponent, selector: "button[uni-text-button], button[text-button]", inputs: ["disable", "loading", "fullWidth", "symbolLeft", "symbolRight"] }, { kind: "component", type: UniCalloutComponent, selector: "uni-callout", inputs: ["open", "key", "target", "placement", "backdrop", "targetInteractive", "dismissible", "dismissOnBackdrop", "header", "arrow", "ariaLabel", "closeLabel"], outputs: ["openChange", "opened", "closed", "dismissed", "panelKeydown"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11266
+ }
11267
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTourComponent, decorators: [{
11268
+ type: Component,
11269
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-tour', imports: [UniButtonComponent, UniCalloutComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'tour' }], template: "<uni-callout\n [(open)]=\"calloutOpen\"\n [key]=\"currentStep()?.key ?? ''\"\n [target]=\"resolvedTarget()\"\n [placement]=\"currentStep()?.placement ?? 'bottom'\"\n [backdrop]=\"currentStep()?.backdrop\"\n [targetInteractive]=\"targetInteractive()\"\n [header]=\"currentStep()?.title ?? ''\"\n [ariaLabel]=\"stepAriaLabel()\"\n [closeLabel]=\"skipLabel()\"\n (dismissed)=\"onDismissed($event)\"\n (panelKeydown)=\"onPanelKeydown($event)\"\n>\n <p>{{ currentStep()?.body }}</p>\n <div callout-actions [class]=\"footerClassName()\">\n @if (index() > 0) {\n <button text-button variant=\"ghost\" (click)=\"back()\">{{ backLabel() }}</button>\n }\n @if (componentOptions().progressStyle === 'fraction') {\n <span aria-hidden=\"true\" [class]=\"fractionClassName\">{{ index() + 1 }} of {{ steps().length }}</span>\n } @else {\n <span aria-hidden=\"true\" [class]=\"dotsClassName()\">\n @for (step of steps(); track $index) {\n <i [class.on]=\"$index === index()\"></i>\n }\n </span>\n }\n <!-- A click-gate advances by using the target itself \u2014 a Next button\n would be a lie, so none renders. -->\n @if (!clickGate()) {\n <button text-button [disable]=\"!satisfied()\" (click)=\"advance()\">\n {{ isLast() ? doneLabel() : nextLabel() }}\n </button>\n }\n </div>\n</uni-callout>\n<!-- One live region per tour: gate unlocks are announced here. -->\n<span role=\"status\" [class]=\"statusClassName\">{{ announcement() }}</span>\n" }]
11270
+ }], ctorParameters: () => [], propDecorators: { steps: [{ type: i0.Input, args: [{ isSignal: true, alias: "steps", required: true }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }, { type: i0.Output, args: ["activeChange"] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], backLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "backLabel", required: false }] }], skipLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipLabel", required: false }] }], doneLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "doneLabel", required: false }] }], started: [{ type: i0.Output, args: ["started"] }], stepChanged: [{ type: i0.Output, args: ["stepChanged"] }], finished: [{ type: i0.Output, args: ["finished"] }], skipped: [{ type: i0.Output, args: ["skipped"] }] } });
11271
+
11272
+ /**
11273
+ * UniTourComponent Barrel File
11274
+ *
11275
+ * This file exports all public-facing elements of the tour component.
11276
+ */
11277
+
9839
11278
  /**
9840
11279
  * Generated bundle index. Do not edit.
9841
11280
  */
9842
11281
 
9843
- export { BodyRenderDirective, ConfirmationDialogComponent, DragAndDropDirective, FOCUSABLE_SELECTOR, ListboxNavigation, LocalStorageService, NotificationService, NotificationsComponent, RippleDirective, ThemeService, UNI_THEMES, UniAlertComponent, UniAppBarComponent, UniAvatarComponent, UniAvatarGroupComponent, UniBackgroundComponent, UniBadgeComponent, UniBaseDatasource, UniBoxComponent, UniBreadcrumbComponent, UniButtonComponent, UniButtonGroupComponent, UniCalendarComponent, UniCardComponent, UniCardContentComponent, UniCardHeaderComponent, UniCenterComponent, UniCheckboxComponent, UniDataSearchComponent, UniDataTableComponent, UniDateInputComponent, UniDateTimeInputComponent, UniDebounceInputComponent, UniDialogButtonsComponent, UniDialogComponent, UniDialogHeaderComponent, UniDividerComponent, UniDrawerComponent, UniDropdownComponent, UniExpandAreaComponent, UniExpandComponent, UniExpandToggleComponent, UniFileDropZoneComponent, UniGridAreaComponent, UniGridComponent, UniIconButtonComponent, UniIconComponent, UniInputBoxComponent, UniInputComponent, UniJsonViewComponent, UniMenuComponent, UniMultiSelectComponent, UniMultiSelectDropdownComponent, UniNotificationBadgeComponent, UniPaginatorComponent, UniPopoverComponent, UniProgressBarComponent, UniProgressGaugeComponent, UniRadioComponent, UniRecordDatasource, UniRowComponent, UniScrollAreaComponent, UniSearchInputComponent, UniSelectComponent, UniServerSideDatasource, UniSkeletonComponent, UniSliderComponent, UniSnackbarComponent, UniSortHeaderComponent, UniStackComponent, UniStatComponent, UniSymbolComponent, UniTabComponent, UniTabsComponent, UniTagComponent, UniTagInputComponent, UniTextComponent, UniTextareaComponent, UniThemeBuilderComponent, UniThemeSwitchComponent, UniTimeInputComponent, UniToggleComponent, UniTooltipComponent, UniWrapComponent, acceptableFile, addDays, addMonths, anchorArrowStyles, anchorStyles, buildMonthGrid, createListboxNavigation, dayOfWeek, daysInMonth, formatDate, formatMonthHeading, formatTime, getFileExtension, inclusiveDayCount, isDivider, isValidDate, isoDate, joinDateTime, localeDatePlaceholder, localeDefaultHour12, localeFieldOrder, localeMonthNames, localeWeekStart, monthOf, motionSafe, newAnchorName, parseDateText, parseTimeText, resolveFocusTarget, splitDateTime, timeSlots, todayIso, uniqueId, useTimer, visuallyHidden, weekdayNames };
11282
+ export { BodyRenderDirective, ConfirmationDialogComponent, DragAndDropDirective, FOCUSABLE_SELECTOR, ListboxNavigation, LocalStorageService, NotificationService, NotificationsComponent, RippleDirective, TRANSFORM_ORIGINS, ThemeService, UNI_THEMES, UniAlertComponent, UniAppBarComponent, UniAvatarComponent, UniAvatarGroupComponent, UniBackgroundComponent, UniBadgeComponent, UniBaseDatasource, UniBoxComponent, UniBreadcrumbComponent, UniButtonComponent, UniButtonGroupComponent, UniCalendarComponent, UniCalloutComponent, UniCardComponent, UniCardContentComponent, UniCardHeaderComponent, UniCenterComponent, UniCheckboxComponent, UniComboboxComponent, UniDataSearchComponent, UniDataTableComponent, UniDateInputComponent, UniDateTimeInputComponent, UniDebounceInputComponent, UniDialogButtonsComponent, UniDialogComponent, UniDialogHeaderComponent, UniDividerComponent, UniDrawerComponent, UniDropdownComponent, UniExpandAreaComponent, UniExpandComponent, UniExpandToggleComponent, UniFileDropZoneComponent, UniGridAreaComponent, UniGridComponent, UniIconButtonComponent, UniIconComponent, UniInputBoxComponent, UniInputComponent, UniJsonViewComponent, UniMenuComponent, UniMultiSelectComponent, UniMultiSelectDropdownComponent, UniNotificationBadgeComponent, UniPaginatorComponent, UniPopoverComponent, UniProgressBarComponent, UniProgressGaugeComponent, UniRadioComponent, UniRecordDatasource, UniRowComponent, UniScrollAreaComponent, UniSearchInputComponent, UniSelectComponent, UniServerSideDatasource, UniSkeletonComponent, UniSliderComponent, UniSnackbarComponent, UniSortHeaderComponent, UniStackComponent, UniStatComponent, UniSymbolComponent, UniTabComponent, UniTabsComponent, UniTagComponent, UniTagInputComponent, UniTextComponent, UniTextareaComponent, UniThemeBuilderComponent, UniThemeSwitchComponent, UniTimeInputComponent, UniToggleComponent, UniTooltipComponent, UniTourComponent, UniWrapComponent, acceptableFile, addDays, addMonths, anchorArrowStyles, anchorStyles, buildMonthGrid, clearAnchorName, createListboxNavigation, dayOfWeek, daysInMonth, discreteOverlayTransition, focusableElements, formatDate, formatMonthHeading, formatTime, getFileExtension, inclusiveDayCount, isDivider, isToggleOpen, isValidDate, isoDate, joinDateTime, listboxPopupStyles, localeDatePlaceholder, localeDefaultHour12, localeFieldOrder, localeMonthNames, localeWeekStart, monthOf, motionSafe, newAnchorName, parseDateText, parseTimeText, resolveElement, resolveFocusTarget, restoreOverlayFocus, setAnchorName, splitDateTime, spotlightStyles, timeSlots, todayIso, transformOriginFor, uniqueId, useTimer, visuallyHidden, weekdayNames };
9844
11283
  //# sourceMappingURL=uni-design-system-uni-angular.mjs.map