@besaitech/ng-design-system 0.0.80 → 0.0.81

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.
@@ -10682,6 +10682,112 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
10682
10682
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], presets: [{ type: i0.Input, args: [{ isSignal: true, alias: "presets", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], maxDays: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDays", required: false }] }], firstDayOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstDayOfWeek", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], months: [{ type: i0.Input, args: [{ isSignal: true, alias: "months", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], panelLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelLabel", required: false }] }], presetsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "presetsLabel", required: false }] }], startLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "startLabel", required: false }] }], endLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "endLabel", required: false }] }], applyLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "applyLabel", required: false }] }], cancelLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "cancelLabel", required: false }] }], calendarLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "calendarLabel", required: false }] }], previousLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "previousLabel", required: false }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], presetSelect: [{ type: i0.Output, args: ["presetSelect"] }], calendar: [{ type: i0.ViewChild, args: [i0.forwardRef(() => SdsRangeCalendar), { isSignal: true }] }], trigger: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }] } });
10683
10683
  let nextId$a = 0;
10684
10684
 
10685
+ /**
10686
+ * Where a floating panel goes, and the plumbing that keeps it there.
10687
+ *
10688
+ * <p><b>Why this exists.</b> Ten components in this library append a panel to `<body>` and position
10689
+ * it against their trigger, and nine of them had their own copy of the same twenty-odd lines. The
10690
+ * copies had already drifted: some clamp the panel inside the viewport vertically and some only
10691
+ * flip it, some match the trigger's width and some do not. Worse, the *lifecycle* was duplicated
10692
+ * too — and `sds-autocomplete` shipped a version that removed the panel through a `viewChild`
10693
+ * already cleared by `ngOnDestroy`, orphaning it in `<body>` for ever. That bug could only exist in
10694
+ * one copy of nine.
10695
+ */
10696
+ /**
10697
+ * The arithmetic, with no DOM in it.
10698
+ *
10699
+ * <p>Vertical: below the trigger, flipped above when it does not fit below and fits better above —
10700
+ * then <b>clamped inside the viewport</b>. The clamp is not decoration: without it a panel taller
10701
+ * than the space either side is pushed off the top edge and its first rows become unreachable, and
10702
+ * four of the nine copies were missing it.
10703
+ *
10704
+ * <p>Horizontal: the panel's start (or end) edge meets the trigger's, then clamped the same way. A
10705
+ * panel wider than the viewport pins to the left margin rather than running off both sides.
10706
+ */
10707
+ function placePanel(trigger, panel, viewport, options = {}) {
10708
+ const gap = options.gap ?? 4;
10709
+ const margin = options.margin ?? 8;
10710
+ const roomBelow = viewport.height - trigger.bottom - gap;
10711
+ const roomAbove = trigger.top - gap;
10712
+ const above = options.prefer === 'above'
10713
+ ? roomAbove >= panel.height || roomAbove > roomBelow
10714
+ : roomBelow < panel.height && roomAbove > roomBelow;
10715
+ const top = clamp(above ? trigger.top - gap - panel.height : trigger.bottom + gap, margin, viewport.height - margin - panel.height);
10716
+ const left = clamp(options.align === 'end' ? trigger.right - panel.width : trigger.left, margin, viewport.width - margin - panel.width);
10717
+ return { top: Math.round(top), left: Math.round(left) };
10718
+ }
10719
+ /**
10720
+ * A viewport bound that may be smaller than the lower one — when the panel is taller than the
10721
+ * window there is no position that satisfies both, and the top edge is the one that must win, or
10722
+ * the panel starts off-screen and cannot be scrolled back.
10723
+ */
10724
+ function clamp(value, low, high) {
10725
+ return Math.max(low, Math.min(value, Math.max(low, high)));
10726
+ }
10727
+ /**
10728
+ * The DOM half: append to `<body>`, place, follow scroll and resize, and — the part that was worth
10729
+ * centralising — take it all down again exactly once.
10730
+ *
10731
+ * <p>Hold one of these per component and call {@link detach} from `ngOnDestroy`. It keeps its own
10732
+ * reference to the panel element, so it does not depend on a `viewChild` that Angular has already
10733
+ * cleared by the time the component is torn down.
10734
+ */
10735
+ class SdsOverlayPanel {
10736
+ trigger = null;
10737
+ panel = null;
10738
+ options = {};
10739
+ listening = false;
10740
+ reflow = () => this.reposition();
10741
+ /** Attach (or re-attach) the panel to the body and start following the trigger. */
10742
+ attach(trigger, panel, options = {}) {
10743
+ this.trigger = trigger;
10744
+ this.panel = panel;
10745
+ this.options = options;
10746
+ if (panel.parentElement !== document.body) {
10747
+ document.body.appendChild(panel);
10748
+ }
10749
+ this.reposition();
10750
+ if (!this.listening) {
10751
+ // `true` for scroll: a panel inside a scrolling container must follow it, and scroll does
10752
+ // not bubble.
10753
+ window.addEventListener('scroll', this.reflow, true);
10754
+ window.addEventListener('resize', this.reflow);
10755
+ this.listening = true;
10756
+ }
10757
+ }
10758
+ reposition() {
10759
+ const trigger = this.trigger;
10760
+ const panel = this.panel;
10761
+ if (!trigger || !panel) {
10762
+ return;
10763
+ }
10764
+ const r = trigger.getBoundingClientRect();
10765
+ if (this.options.matchTriggerWidth) {
10766
+ panel.style.minWidth = `${Math.round(r.width)}px`;
10767
+ }
10768
+ const { top, left } = placePanel(r,
10769
+ // offset* is 0 before the panel has laid out; the fallbacks keep the first frame sane.
10770
+ { width: panel.offsetWidth || r.width, height: panel.offsetHeight || 240 }, { width: window.innerWidth, height: window.innerHeight }, this.options);
10771
+ panel.style.top = `${top}px`;
10772
+ panel.style.left = `${left}px`;
10773
+ }
10774
+ /** Does the panel contain this node? For "did the click land outside" checks. */
10775
+ contains(node) {
10776
+ return !!node && !!this.panel?.contains(node);
10777
+ }
10778
+ /** Stop following, and remove the panel from the body. Safe to call more than once. */
10779
+ detach() {
10780
+ if (this.listening) {
10781
+ window.removeEventListener('scroll', this.reflow, true);
10782
+ window.removeEventListener('resize', this.reflow);
10783
+ this.listening = false;
10784
+ }
10785
+ this.panel?.remove();
10786
+ this.panel = null;
10787
+ this.trigger = null;
10788
+ }
10789
+ }
10790
+
10685
10791
  /**
10686
10792
  * Supplies a custom template for each option row (and the selected value shown
10687
10793
  * in the trigger) of an `<sds-select>` / `<sds-multi-select>`. Use as a
@@ -10838,6 +10944,8 @@ class SdsSelect {
10838
10944
  cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : []));
10839
10945
  trigger = viewChild('trigger', ...(ngDevMode ? [{ debugName: "trigger" }] : []));
10840
10946
  panelEl = null;
10947
+ /** Body-appending, positioning, follow-on-scroll and teardown — see SdsOverlayPanel. */
10948
+ overlay = new SdsOverlayPanel();
10841
10949
  isDisabled = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
10842
10950
  filled = computed(() => this.appearance() === 'filled', ...(ngDevMode ? [{ debugName: "filled" }] : []));
10843
10951
  onBrand = computed(() => this.appearance() === 'on-brand', ...(ngDevMode ? [{ debugName: "onBrand" }] : []));
@@ -10882,9 +10990,7 @@ class SdsSelect {
10882
10990
  ? 'border-danger focus:border-danger focus:ring-2 focus:ring-danger/25'
10883
10991
  : 'border-border focus:border-brand-500 focus:ring-2 focus:ring-brand-500/25');
10884
10992
  }
10885
- parts.push(this.isDisabled()
10886
- ? 'opacity-60 cursor-not-allowed pointer-events-none'
10887
- : 'cursor-pointer');
10993
+ parts.push(this.isDisabled() ? 'opacity-60 cursor-not-allowed pointer-events-none' : 'cursor-pointer');
10888
10994
  return parts.join(' ');
10889
10995
  }, ...(ngDevMode ? [{ debugName: "wrapperClasses" }] : []));
10890
10996
  // --- option normalization & equality ---
@@ -10918,7 +11024,11 @@ class SdsSelect {
10918
11024
  }, ...(ngDevMode ? [{ debugName: "triggerContext" }] : []));
10919
11025
  /** Context for a projected footer template. */
10920
11026
  footerContext() {
10921
- return { $implicit: this.filterText(), filterText: this.filterText(), close: () => this.close(true) };
11027
+ return {
11028
+ $implicit: this.filterText(),
11029
+ filterText: this.filterText(),
11030
+ close: () => this.close(true),
11031
+ };
10922
11032
  }
10923
11033
  equals(a, b) {
10924
11034
  const k = this.dataKey();
@@ -10958,9 +11068,9 @@ class SdsSelect {
10958
11068
  this.onTouched();
10959
11069
  this.initHighlight();
10960
11070
  afterNextRender(() => {
10961
- this.panelEl = this.host.nativeElement.querySelector('[data-sds-select-panel]');
10962
- this.positionPanel();
10963
- this.addReflowListeners();
11071
+ this.panelEl =
11072
+ this.host.nativeElement.querySelector('[data-sds-select-panel]');
11073
+ this.attachPanel();
10964
11074
  if (this.filter()) {
10965
11075
  this.panelEl?.querySelector('input')?.focus();
10966
11076
  }
@@ -10973,8 +11083,7 @@ class SdsSelect {
10973
11083
  close(returnFocus = false) {
10974
11084
  if (!this.open())
10975
11085
  return;
10976
- this.removeReflowListeners();
10977
- this.panelEl?.remove();
11086
+ this.overlay.detach();
10978
11087
  this.panelEl = null;
10979
11088
  this.open.set(false);
10980
11089
  if (returnFocus)
@@ -10991,7 +11100,7 @@ class SdsSelect {
10991
11100
  onFilter(event) {
10992
11101
  this.filterText.set(event.target.value);
10993
11102
  this.highlightFirst();
10994
- setTimeout(() => this.positionPanel());
11103
+ setTimeout(() => this.overlay.reposition());
10995
11104
  }
10996
11105
  // --- keyboard ---
10997
11106
  onTriggerKeydown(event) {
@@ -11047,7 +11156,8 @@ class SdsSelect {
11047
11156
  case 'Tab':
11048
11157
  this.close(false);
11049
11158
  return true;
11050
- default: return false;
11159
+ default:
11160
+ return false;
11051
11161
  }
11052
11162
  }
11053
11163
  initHighlight() {
@@ -11110,46 +11220,12 @@ class SdsSelect {
11110
11220
  el?.scrollIntoView({ block: 'nearest' });
11111
11221
  });
11112
11222
  }
11113
- // --- body-appended overlay positioning (mirrors sds-date-picker) ---
11114
- reflow = () => this.positionPanel();
11115
- listening = false;
11116
- addReflowListeners() {
11117
- if (this.listening)
11118
- return;
11119
- window.addEventListener('scroll', this.reflow, true);
11120
- window.addEventListener('resize', this.reflow);
11121
- this.listening = true;
11122
- }
11123
- removeReflowListeners() {
11124
- if (!this.listening)
11125
- return;
11126
- window.removeEventListener('scroll', this.reflow, true);
11127
- window.removeEventListener('resize', this.reflow);
11128
- this.listening = false;
11129
- }
11130
- positionPanel() {
11131
- const triggerEl = this.trigger()?.nativeElement;
11132
- const panelEl = this.panelEl;
11133
- if (!triggerEl || !panelEl)
11134
- return;
11135
- if (panelEl.parentElement !== document.body)
11136
- document.body.appendChild(panelEl);
11137
- const r = triggerEl.getBoundingClientRect();
11138
- panelEl.style.minWidth = `${Math.round(r.width)}px`;
11139
- const ph = panelEl.offsetHeight || 240;
11140
- const gap = 4;
11141
- const margin = 8;
11142
- let top = r.bottom + gap;
11143
- if (top + ph > window.innerHeight - margin && r.top - gap - ph > margin) {
11144
- top = r.top - gap - ph;
11223
+ /** Attach once the panel exists — it is rendered inside an `@if`. */
11224
+ attachPanel() {
11225
+ const trigger = this.trigger()?.nativeElement;
11226
+ if (trigger && this.panelEl) {
11227
+ this.overlay.attach(trigger, this.panelEl, { matchTriggerWidth: true });
11145
11228
  }
11146
- let left = r.left;
11147
- const pw = panelEl.offsetWidth || r.width;
11148
- if (left + pw > window.innerWidth - margin) {
11149
- left = Math.max(margin, window.innerWidth - margin - pw);
11150
- }
11151
- panelEl.style.top = `${Math.round(top)}px`;
11152
- panelEl.style.left = `${Math.round(left)}px`;
11153
11229
  }
11154
11230
  onDocumentClick(event) {
11155
11231
  if (!this.open())
@@ -11161,9 +11237,8 @@ class SdsSelect {
11161
11237
  this.close();
11162
11238
  }
11163
11239
  ngOnDestroy() {
11164
- this.removeReflowListeners();
11165
11240
  clearTimeout(this.typeTimer);
11166
- this.panelEl?.remove();
11241
+ this.overlay.detach();
11167
11242
  }
11168
11243
  setValue(v) {
11169
11244
  this.value.set(v);
@@ -11224,10 +11299,7 @@ class SdsSelect {
11224
11299
  />
11225
11300
  </span>
11226
11301
  } @else {
11227
- <span
11228
- class="flex-1 truncate pl-3.5"
11229
- [class]="valueTextClass()"
11230
- >
11302
+ <span class="flex-1 truncate pl-3.5" [class]="valueTextClass()">
11231
11303
  {{ selectedOption() ? selectedOption()!.label : placeholder() }}
11232
11304
  </span>
11233
11305
  }
@@ -11238,7 +11310,11 @@ class SdsSelect {
11238
11310
  tabindex="-1"
11239
11311
  [attr.aria-label]="clearLabel()"
11240
11312
  class="mr-1 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full"
11241
- [class]="onBrand() ? 'text-white/60 hover:bg-white/10 hover:text-white' : 'text-ink-faint hover:bg-black/5 hover:text-ink-soft'"
11313
+ [class]="
11314
+ onBrand()
11315
+ ? 'text-white/60 hover:bg-white/10 hover:text-white'
11316
+ : 'text-ink-faint hover:bg-black/5 hover:text-ink-soft'
11317
+ "
11242
11318
  (click)="clear($event)"
11243
11319
  >
11244
11320
  <sds-icon name="x" [size]="15" />
@@ -11257,11 +11333,20 @@ class SdsSelect {
11257
11333
 
11258
11334
  @if (open()) {
11259
11335
  <!-- Appended to document.body on open (see positionPanel). -->
11260
- <div data-sds-select-panel class="fixed z-[1000] overflow-hidden rounded-card border border-border bg-surface shadow-lg">
11336
+ <div
11337
+ data-sds-select-panel
11338
+ class="fixed z-[1000] overflow-hidden rounded-card border border-border bg-surface shadow-lg"
11339
+ >
11261
11340
  @if (filter()) {
11262
11341
  <div class="border-b border-border p-2">
11263
- <div class="relative flex items-center rounded-control border border-border focus-within:border-brand-500">
11264
- <sds-icon name="search" [size]="16" class="pointer-events-none absolute left-2.5 text-ink-faint" />
11342
+ <div
11343
+ class="relative flex items-center rounded-control border border-border focus-within:border-brand-500"
11344
+ >
11345
+ <sds-icon
11346
+ name="search"
11347
+ [size]="16"
11348
+ class="pointer-events-none absolute left-2.5 text-ink-faint"
11349
+ />
11265
11350
  <input
11266
11351
  type="text"
11267
11352
  role="combobox"
@@ -11310,7 +11395,10 @@ class SdsSelect {
11310
11395
  <li class="px-3 py-2 text-sm text-ink-faint">
11311
11396
  <ng-container
11312
11397
  [ngTemplateOutlet]="emptyTpl()!.template"
11313
- [ngTemplateOutletContext]="{ $implicit: filterText(), filterText: filterText() }"
11398
+ [ngTemplateOutletContext]="{
11399
+ $implicit: filterText(),
11400
+ filterText: filterText(),
11401
+ }"
11314
11402
  />
11315
11403
  </li>
11316
11404
  } @else {
@@ -11386,10 +11474,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
11386
11474
  />
11387
11475
  </span>
11388
11476
  } @else {
11389
- <span
11390
- class="flex-1 truncate pl-3.5"
11391
- [class]="valueTextClass()"
11392
- >
11477
+ <span class="flex-1 truncate pl-3.5" [class]="valueTextClass()">
11393
11478
  {{ selectedOption() ? selectedOption()!.label : placeholder() }}
11394
11479
  </span>
11395
11480
  }
@@ -11400,7 +11485,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
11400
11485
  tabindex="-1"
11401
11486
  [attr.aria-label]="clearLabel()"
11402
11487
  class="mr-1 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full"
11403
- [class]="onBrand() ? 'text-white/60 hover:bg-white/10 hover:text-white' : 'text-ink-faint hover:bg-black/5 hover:text-ink-soft'"
11488
+ [class]="
11489
+ onBrand()
11490
+ ? 'text-white/60 hover:bg-white/10 hover:text-white'
11491
+ : 'text-ink-faint hover:bg-black/5 hover:text-ink-soft'
11492
+ "
11404
11493
  (click)="clear($event)"
11405
11494
  >
11406
11495
  <sds-icon name="x" [size]="15" />
@@ -11419,11 +11508,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
11419
11508
 
11420
11509
  @if (open()) {
11421
11510
  <!-- Appended to document.body on open (see positionPanel). -->
11422
- <div data-sds-select-panel class="fixed z-[1000] overflow-hidden rounded-card border border-border bg-surface shadow-lg">
11511
+ <div
11512
+ data-sds-select-panel
11513
+ class="fixed z-[1000] overflow-hidden rounded-card border border-border bg-surface shadow-lg"
11514
+ >
11423
11515
  @if (filter()) {
11424
11516
  <div class="border-b border-border p-2">
11425
- <div class="relative flex items-center rounded-control border border-border focus-within:border-brand-500">
11426
- <sds-icon name="search" [size]="16" class="pointer-events-none absolute left-2.5 text-ink-faint" />
11517
+ <div
11518
+ class="relative flex items-center rounded-control border border-border focus-within:border-brand-500"
11519
+ >
11520
+ <sds-icon
11521
+ name="search"
11522
+ [size]="16"
11523
+ class="pointer-events-none absolute left-2.5 text-ink-faint"
11524
+ />
11427
11525
  <input
11428
11526
  type="text"
11429
11527
  role="combobox"
@@ -11472,7 +11570,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
11472
11570
  <li class="px-3 py-2 text-sm text-ink-faint">
11473
11571
  <ng-container
11474
11572
  [ngTemplateOutlet]="emptyTpl()!.template"
11475
- [ngTemplateOutletContext]="{ $implicit: filterText(), filterText: filterText() }"
11573
+ [ngTemplateOutletContext]="{
11574
+ $implicit: filterText(),
11575
+ filterText: filterText(),
11576
+ }"
11476
11577
  />
11477
11578
  </li>
11478
11579
  } @else {
@@ -11596,6 +11697,8 @@ class SdsMultiSelect {
11596
11697
  cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : []));
11597
11698
  trigger = viewChild('trigger', ...(ngDevMode ? [{ debugName: "trigger" }] : []));
11598
11699
  panelEl = null;
11700
+ /** Body-appending, positioning, follow-on-scroll and teardown — see SdsOverlayPanel. */
11701
+ overlay = new SdsOverlayPanel();
11599
11702
  isDisabled = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
11600
11703
  normalizedOptions = computed(() => this.options().map((o) => this.norm(o)), ...(ngDevMode ? [{ debugName: "normalizedOptions" }] : []));
11601
11704
  filteredOptions = computed(() => {
@@ -11641,9 +11744,7 @@ class SdsMultiSelect {
11641
11744
  parts.push(this.error()
11642
11745
  ? 'border-danger focus:border-danger focus:ring-2 focus:ring-danger/25'
11643
11746
  : 'border-border focus:border-brand-500 focus:ring-2 focus:ring-brand-500/25');
11644
- parts.push(this.isDisabled()
11645
- ? 'opacity-60 cursor-not-allowed pointer-events-none'
11646
- : 'cursor-pointer');
11747
+ parts.push(this.isDisabled() ? 'opacity-60 cursor-not-allowed pointer-events-none' : 'cursor-pointer');
11647
11748
  return parts.join(' ');
11648
11749
  }, ...(ngDevMode ? [{ debugName: "wrapperClasses" }] : []));
11649
11750
  // --- normalization & equality ---
@@ -11672,7 +11773,11 @@ class SdsMultiSelect {
11672
11773
  }
11673
11774
  /** Context for a projected footer template. */
11674
11775
  footerContext() {
11675
- return { $implicit: this.filterText(), filterText: this.filterText(), close: () => this.close(true) };
11776
+ return {
11777
+ $implicit: this.filterText(),
11778
+ filterText: this.filterText(),
11779
+ close: () => this.close(true),
11780
+ };
11676
11781
  }
11677
11782
  equals(a, b) {
11678
11783
  const k = this.dataKey();
@@ -11718,9 +11823,9 @@ class SdsMultiSelect {
11718
11823
  this.onTouched();
11719
11824
  this.initHighlight();
11720
11825
  afterNextRender(() => {
11721
- this.panelEl = this.host.nativeElement.querySelector('[data-sds-select-panel]');
11722
- this.positionPanel();
11723
- this.addReflowListeners();
11826
+ this.panelEl =
11827
+ this.host.nativeElement.querySelector('[data-sds-select-panel]');
11828
+ this.attachPanel();
11724
11829
  if (this.filter()) {
11725
11830
  this.panelEl?.querySelector('input')?.focus();
11726
11831
  }
@@ -11733,8 +11838,7 @@ class SdsMultiSelect {
11733
11838
  close(returnFocus = false) {
11734
11839
  if (!this.open())
11735
11840
  return;
11736
- this.removeReflowListeners();
11737
- this.panelEl?.remove();
11841
+ this.overlay.detach();
11738
11842
  this.panelEl = null;
11739
11843
  this.open.set(false);
11740
11844
  if (returnFocus)
@@ -11752,9 +11856,7 @@ class SdsMultiSelect {
11752
11856
  toggleOption(opt) {
11753
11857
  const cur = this.value();
11754
11858
  const exists = cur.some((x) => this.equals(x, opt.value));
11755
- const next = exists
11756
- ? cur.filter((x) => !this.equals(x, opt.value))
11757
- : [...cur, opt.value];
11859
+ const next = exists ? cur.filter((x) => !this.equals(x, opt.value)) : [...cur, opt.value];
11758
11860
  this.setValue(next);
11759
11861
  }
11760
11862
  toggleAll() {
@@ -11770,7 +11872,7 @@ class SdsMultiSelect {
11770
11872
  onFilter(event) {
11771
11873
  this.filterText.set(event.target.value);
11772
11874
  this.highlightFirst();
11773
- setTimeout(() => this.positionPanel());
11875
+ setTimeout(() => this.overlay.reposition());
11774
11876
  }
11775
11877
  // --- keyboard ---
11776
11878
  onTriggerKeydown(event) {
@@ -11835,7 +11937,8 @@ class SdsMultiSelect {
11835
11937
  case 'Tab':
11836
11938
  this.close(false);
11837
11939
  return true;
11838
- default: return false;
11940
+ default:
11941
+ return false;
11839
11942
  }
11840
11943
  }
11841
11944
  initHighlight() {
@@ -11903,46 +12006,12 @@ class SdsMultiSelect {
11903
12006
  el?.scrollIntoView({ block: 'nearest' });
11904
12007
  });
11905
12008
  }
11906
- // --- body-appended overlay positioning (mirrors sds-select) ---
11907
- reflow = () => this.positionPanel();
11908
- listening = false;
11909
- addReflowListeners() {
11910
- if (this.listening)
11911
- return;
11912
- window.addEventListener('scroll', this.reflow, true);
11913
- window.addEventListener('resize', this.reflow);
11914
- this.listening = true;
11915
- }
11916
- removeReflowListeners() {
11917
- if (!this.listening)
11918
- return;
11919
- window.removeEventListener('scroll', this.reflow, true);
11920
- window.removeEventListener('resize', this.reflow);
11921
- this.listening = false;
11922
- }
11923
- positionPanel() {
11924
- const triggerEl = this.trigger()?.nativeElement;
11925
- const panelEl = this.panelEl;
11926
- if (!triggerEl || !panelEl)
11927
- return;
11928
- if (panelEl.parentElement !== document.body)
11929
- document.body.appendChild(panelEl);
11930
- const r = triggerEl.getBoundingClientRect();
11931
- panelEl.style.minWidth = `${Math.round(r.width)}px`;
11932
- const ph = panelEl.offsetHeight || 240;
11933
- const gap = 4;
11934
- const margin = 8;
11935
- let top = r.bottom + gap;
11936
- if (top + ph > window.innerHeight - margin && r.top - gap - ph > margin) {
11937
- top = r.top - gap - ph;
11938
- }
11939
- let left = r.left;
11940
- const pw = panelEl.offsetWidth || r.width;
11941
- if (left + pw > window.innerWidth - margin) {
11942
- left = Math.max(margin, window.innerWidth - margin - pw);
12009
+ /** Attach once the panel exists — it is rendered inside an `@if`. */
12010
+ attachPanel() {
12011
+ const trigger = this.trigger()?.nativeElement;
12012
+ if (trigger && this.panelEl) {
12013
+ this.overlay.attach(trigger, this.panelEl, { matchTriggerWidth: true });
11943
12014
  }
11944
- panelEl.style.top = `${Math.round(top)}px`;
11945
- panelEl.style.left = `${Math.round(left)}px`;
11946
12015
  }
11947
12016
  onDocumentClick(event) {
11948
12017
  if (!this.open())
@@ -11954,9 +12023,8 @@ class SdsMultiSelect {
11954
12023
  this.close();
11955
12024
  }
11956
12025
  ngOnDestroy() {
11957
- this.removeReflowListeners();
11958
12026
  clearTimeout(this.typeTimer);
11959
- this.panelEl?.remove();
12027
+ this.overlay.detach();
11960
12028
  }
11961
12029
  setValue(v) {
11962
12030
  this.value.set(v);
@@ -11964,7 +12032,7 @@ class SdsMultiSelect {
11964
12032
  this.onTouched();
11965
12033
  // Chip display can change the trigger height while the panel is open.
11966
12034
  if (this.open())
11967
- setTimeout(() => this.positionPanel());
12035
+ setTimeout(() => this.overlay.reposition());
11968
12036
  }
11969
12037
  // --- ControlValueAccessor ---
11970
12038
  onChange = () => { };
@@ -12012,7 +12080,9 @@ class SdsMultiSelect {
12012
12080
  @if (display() === 'chip' && selectedOptions().length) {
12013
12081
  <div class="flex flex-1 flex-wrap items-center gap-1 py-1 pl-2">
12014
12082
  @for (opt of selectedOptions(); track $index) {
12015
- <span class="inline-flex items-center gap-1 rounded bg-brand-50 py-0.5 pl-2 pr-1 text-xs font-medium text-brand-700">
12083
+ <span
12084
+ class="inline-flex items-center gap-1 rounded bg-brand-50 py-0.5 pl-2 pr-1 text-xs font-medium text-brand-700"
12085
+ >
12016
12086
  {{ opt.label }}
12017
12087
  <button
12018
12088
  type="button"
@@ -12057,11 +12127,20 @@ class SdsMultiSelect {
12057
12127
 
12058
12128
  @if (open()) {
12059
12129
  <!-- Appended to document.body on open (see positionPanel). -->
12060
- <div data-sds-select-panel class="fixed z-[1000] overflow-hidden rounded-card border border-border bg-surface shadow-lg">
12130
+ <div
12131
+ data-sds-select-panel
12132
+ class="fixed z-[1000] overflow-hidden rounded-card border border-border bg-surface shadow-lg"
12133
+ >
12061
12134
  @if (filter()) {
12062
12135
  <div class="border-b border-border p-2">
12063
- <div class="relative flex items-center rounded-control border border-border focus-within:border-brand-500">
12064
- <sds-icon name="search" [size]="16" class="pointer-events-none absolute left-2.5 text-ink-faint" />
12136
+ <div
12137
+ class="relative flex items-center rounded-control border border-border focus-within:border-brand-500"
12138
+ >
12139
+ <sds-icon
12140
+ name="search"
12141
+ [size]="16"
12142
+ class="pointer-events-none absolute left-2.5 text-ink-faint"
12143
+ />
12065
12144
  <input
12066
12145
  type="text"
12067
12146
  role="combobox"
@@ -12085,13 +12164,18 @@ class SdsMultiSelect {
12085
12164
  role="checkbox"
12086
12165
  [id]="toggleAllId"
12087
12166
  [attr.data-index]="-1"
12088
- [attr.aria-checked]="allFilteredSelected() ? 'true' : (toggleAllIndeterminate() ? 'mixed' : 'false')"
12167
+ [attr.aria-checked]="
12168
+ allFilteredSelected() ? 'true' : toggleAllIndeterminate() ? 'mixed' : 'false'
12169
+ "
12089
12170
  class="flex w-full items-center gap-2.5 border-b border-border px-3 py-2 text-sm text-ink"
12090
12171
  [class.bg-brand-50]="highlightedIndex() === -1"
12091
12172
  (click)="toggleAll()"
12092
12173
  (mouseenter)="highlightedIndex.set(-1)"
12093
12174
  >
12094
- <span class="grid h-[18px] w-[18px] shrink-0 place-items-center rounded border" [class]="checkboxClass(allFilteredSelected(), toggleAllIndeterminate())">
12175
+ <span
12176
+ class="grid h-[18px] w-[18px] shrink-0 place-items-center rounded border"
12177
+ [class]="checkboxClass(allFilteredSelected(), toggleAllIndeterminate())"
12178
+ >
12095
12179
  @if (allFilteredSelected()) {
12096
12180
  <sds-icon name="check" [size]="12" />
12097
12181
  } @else if (toggleAllIndeterminate()) {
@@ -12102,7 +12186,12 @@ class SdsMultiSelect {
12102
12186
  </button>
12103
12187
  }
12104
12188
 
12105
- <ul [id]="listId" role="listbox" aria-multiselectable="true" class="max-h-60 overflow-auto py-1">
12189
+ <ul
12190
+ [id]="listId"
12191
+ role="listbox"
12192
+ aria-multiselectable="true"
12193
+ class="max-h-60 overflow-auto py-1"
12194
+ >
12106
12195
  @for (opt of filteredOptions(); track $index; let i = $index) {
12107
12196
  <li
12108
12197
  [id]="optionId(i)"
@@ -12114,7 +12203,10 @@ class SdsMultiSelect {
12114
12203
  (click)="opt.disabled ? null : toggleOption(opt)"
12115
12204
  (mouseenter)="opt.disabled ? null : highlightedIndex.set(i)"
12116
12205
  >
12117
- <span class="grid h-[18px] w-[18px] shrink-0 place-items-center rounded border" [class]="checkboxClass(isSelected(opt.value), false)">
12206
+ <span
12207
+ class="grid h-[18px] w-[18px] shrink-0 place-items-center rounded border"
12208
+ [class]="checkboxClass(isSelected(opt.value), false)"
12209
+ >
12118
12210
  @if (isSelected(opt.value)) {
12119
12211
  <sds-icon name="check" [size]="12" />
12120
12212
  }
@@ -12135,7 +12227,10 @@ class SdsMultiSelect {
12135
12227
  <li class="px-3 py-2 text-sm text-ink-faint">
12136
12228
  <ng-container
12137
12229
  [ngTemplateOutlet]="emptyTpl()!.template"
12138
- [ngTemplateOutletContext]="{ $implicit: filterText(), filterText: filterText() }"
12230
+ [ngTemplateOutletContext]="{
12231
+ $implicit: filterText(),
12232
+ filterText: filterText(),
12233
+ }"
12139
12234
  />
12140
12235
  </li>
12141
12236
  } @else {
@@ -12203,7 +12298,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
12203
12298
  @if (display() === 'chip' && selectedOptions().length) {
12204
12299
  <div class="flex flex-1 flex-wrap items-center gap-1 py-1 pl-2">
12205
12300
  @for (opt of selectedOptions(); track $index) {
12206
- <span class="inline-flex items-center gap-1 rounded bg-brand-50 py-0.5 pl-2 pr-1 text-xs font-medium text-brand-700">
12301
+ <span
12302
+ class="inline-flex items-center gap-1 rounded bg-brand-50 py-0.5 pl-2 pr-1 text-xs font-medium text-brand-700"
12303
+ >
12207
12304
  {{ opt.label }}
12208
12305
  <button
12209
12306
  type="button"
@@ -12248,11 +12345,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
12248
12345
 
12249
12346
  @if (open()) {
12250
12347
  <!-- Appended to document.body on open (see positionPanel). -->
12251
- <div data-sds-select-panel class="fixed z-[1000] overflow-hidden rounded-card border border-border bg-surface shadow-lg">
12348
+ <div
12349
+ data-sds-select-panel
12350
+ class="fixed z-[1000] overflow-hidden rounded-card border border-border bg-surface shadow-lg"
12351
+ >
12252
12352
  @if (filter()) {
12253
12353
  <div class="border-b border-border p-2">
12254
- <div class="relative flex items-center rounded-control border border-border focus-within:border-brand-500">
12255
- <sds-icon name="search" [size]="16" class="pointer-events-none absolute left-2.5 text-ink-faint" />
12354
+ <div
12355
+ class="relative flex items-center rounded-control border border-border focus-within:border-brand-500"
12356
+ >
12357
+ <sds-icon
12358
+ name="search"
12359
+ [size]="16"
12360
+ class="pointer-events-none absolute left-2.5 text-ink-faint"
12361
+ />
12256
12362
  <input
12257
12363
  type="text"
12258
12364
  role="combobox"
@@ -12276,13 +12382,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
12276
12382
  role="checkbox"
12277
12383
  [id]="toggleAllId"
12278
12384
  [attr.data-index]="-1"
12279
- [attr.aria-checked]="allFilteredSelected() ? 'true' : (toggleAllIndeterminate() ? 'mixed' : 'false')"
12385
+ [attr.aria-checked]="
12386
+ allFilteredSelected() ? 'true' : toggleAllIndeterminate() ? 'mixed' : 'false'
12387
+ "
12280
12388
  class="flex w-full items-center gap-2.5 border-b border-border px-3 py-2 text-sm text-ink"
12281
12389
  [class.bg-brand-50]="highlightedIndex() === -1"
12282
12390
  (click)="toggleAll()"
12283
12391
  (mouseenter)="highlightedIndex.set(-1)"
12284
12392
  >
12285
- <span class="grid h-[18px] w-[18px] shrink-0 place-items-center rounded border" [class]="checkboxClass(allFilteredSelected(), toggleAllIndeterminate())">
12393
+ <span
12394
+ class="grid h-[18px] w-[18px] shrink-0 place-items-center rounded border"
12395
+ [class]="checkboxClass(allFilteredSelected(), toggleAllIndeterminate())"
12396
+ >
12286
12397
  @if (allFilteredSelected()) {
12287
12398
  <sds-icon name="check" [size]="12" />
12288
12399
  } @else if (toggleAllIndeterminate()) {
@@ -12293,7 +12404,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
12293
12404
  </button>
12294
12405
  }
12295
12406
 
12296
- <ul [id]="listId" role="listbox" aria-multiselectable="true" class="max-h-60 overflow-auto py-1">
12407
+ <ul
12408
+ [id]="listId"
12409
+ role="listbox"
12410
+ aria-multiselectable="true"
12411
+ class="max-h-60 overflow-auto py-1"
12412
+ >
12297
12413
  @for (opt of filteredOptions(); track $index; let i = $index) {
12298
12414
  <li
12299
12415
  [id]="optionId(i)"
@@ -12305,7 +12421,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
12305
12421
  (click)="opt.disabled ? null : toggleOption(opt)"
12306
12422
  (mouseenter)="opt.disabled ? null : highlightedIndex.set(i)"
12307
12423
  >
12308
- <span class="grid h-[18px] w-[18px] shrink-0 place-items-center rounded border" [class]="checkboxClass(isSelected(opt.value), false)">
12424
+ <span
12425
+ class="grid h-[18px] w-[18px] shrink-0 place-items-center rounded border"
12426
+ [class]="checkboxClass(isSelected(opt.value), false)"
12427
+ >
12309
12428
  @if (isSelected(opt.value)) {
12310
12429
  <sds-icon name="check" [size]="12" />
12311
12430
  }
@@ -12326,7 +12445,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
12326
12445
  <li class="px-3 py-2 text-sm text-ink-faint">
12327
12446
  <ng-container
12328
12447
  [ngTemplateOutlet]="emptyTpl()!.template"
12329
- [ngTemplateOutletContext]="{ $implicit: filterText(), filterText: filterText() }"
12448
+ [ngTemplateOutletContext]="{
12449
+ $implicit: filterText(),
12450
+ filterText: filterText(),
12451
+ }"
12330
12452
  />
12331
12453
  </li>
12332
12454
  } @else {
@@ -12450,15 +12572,8 @@ class SdsAutocomplete {
12450
12572
  cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : []));
12451
12573
  /** Every option this control has seen, by value — how a chip keeps its label after a new search. */
12452
12574
  seen = new Map();
12453
- /**
12454
- * A plain reference to the body-appended panel, not just the viewChild.
12455
- *
12456
- * <p>On destroy the viewChild is already cleared — it lives inside an `@if` — so removing through
12457
- * it removes nothing and the panel is orphaned in `<body>` for ever. A test caught it: a leaked
12458
- * panel from one spec was still in the document when the next one queried for its own.
12459
- * `sds-select` keeps the same raw handle, for the same reason.
12460
- */
12461
- panelEl = null;
12575
+ /** Body-appending, positioning, follow-on-scroll and teardown all live in here now. */
12576
+ overlay = new SdsOverlayPanel();
12462
12577
  timer;
12463
12578
  seq = 0;
12464
12579
  destroyed = false;
@@ -12490,7 +12605,7 @@ class SdsAutocomplete {
12490
12605
  }, ...(ngDevMode ? [{ debugName: "triggerPlaceholder" }] : []));
12491
12606
  closeFn = () => this.close();
12492
12607
  constructor() {
12493
- afterNextRender(() => this.position(), { injector: this.injector });
12608
+ afterNextRender(() => this.place(), { injector: this.injector });
12494
12609
  }
12495
12610
  // ------------------------------------------------------------------ options
12496
12611
  labelOf(option) {
@@ -12553,8 +12668,7 @@ class SdsAutocomplete {
12553
12668
  this.open.set(true);
12554
12669
  this.opened.emit();
12555
12670
  this.schedule(0);
12556
- setTimeout(() => this.position());
12557
- this.addReflow();
12671
+ setTimeout(() => this.place());
12558
12672
  }
12559
12673
  toggle(event) {
12560
12674
  event.stopPropagation();
@@ -12598,7 +12712,8 @@ class SdsAutocomplete {
12598
12712
  finally {
12599
12713
  if (seq === this.seq && !this.destroyed) {
12600
12714
  this.loading.set(false);
12601
- setTimeout(() => this.position());
12715
+ // The panel's height changed with its contents, so it may now need flipping.
12716
+ setTimeout(() => this.overlay.reposition());
12602
12717
  }
12603
12718
  }
12604
12719
  }
@@ -12682,67 +12797,29 @@ class SdsAutocomplete {
12682
12797
  return;
12683
12798
  this.open.set(false);
12684
12799
  this.active.set(-1);
12685
- this.removeReflow();
12686
- this.panelEl?.remove();
12687
- this.panelEl = null;
12800
+ this.overlay.detach();
12688
12801
  this.closed.emit(this.value());
12689
12802
  }
12690
12803
  onDocumentClick(event) {
12691
12804
  if (!this.open())
12692
12805
  return;
12693
12806
  const target = event.target;
12694
- if (!this.host.nativeElement.contains(target) && !this.panelEl?.contains(target)) {
12807
+ if (!this.host.nativeElement.contains(target) && !this.overlay.contains(target)) {
12695
12808
  this.close();
12696
12809
  }
12697
12810
  }
12698
- // --- body-appended overlay positioning (mirrors sds-select) ---
12699
- reflow = () => this.position();
12700
- listening = false;
12701
- addReflow() {
12702
- if (this.listening)
12703
- return;
12704
- window.addEventListener('scroll', this.reflow, true);
12705
- window.addEventListener('resize', this.reflow);
12706
- this.listening = true;
12707
- }
12708
- removeReflow() {
12709
- if (!this.listening)
12710
- return;
12711
- window.removeEventListener('scroll', this.reflow, true);
12712
- window.removeEventListener('resize', this.reflow);
12713
- this.listening = false;
12714
- }
12715
- position() {
12811
+ /** Attach once the panel exists in the DOM — it is inside an `@if`. */
12812
+ place() {
12716
12813
  const trigger = this.triggerEl()?.nativeElement;
12717
12814
  const panel = this.panelRef()?.nativeElement;
12718
- if (!trigger || !panel)
12719
- return;
12720
- this.panelEl = panel;
12721
- if (panel.parentElement !== document.body)
12722
- document.body.appendChild(panel);
12723
- const r = trigger.getBoundingClientRect();
12724
- panel.style.minWidth = `${Math.round(r.width)}px`;
12725
- const ph = panel.offsetHeight || 240;
12726
- const gap = 4;
12727
- const margin = 8;
12728
- let top = r.bottom + gap;
12729
- if (top + ph > window.innerHeight - margin && r.top - gap - ph > margin) {
12730
- top = r.top - gap - ph;
12815
+ if (trigger && panel) {
12816
+ this.overlay.attach(trigger, panel, { matchTriggerWidth: true });
12731
12817
  }
12732
- let left = r.left;
12733
- const pw = panel.offsetWidth || r.width;
12734
- if (left + pw > window.innerWidth - margin) {
12735
- left = Math.max(margin, window.innerWidth - margin - pw);
12736
- }
12737
- panel.style.top = `${Math.round(top)}px`;
12738
- panel.style.left = `${Math.round(left)}px`;
12739
12818
  }
12740
12819
  ngOnDestroy() {
12741
12820
  this.destroyed = true;
12742
12821
  clearTimeout(this.timer);
12743
- this.removeReflow();
12744
- this.panelEl?.remove();
12745
- this.panelEl = null;
12822
+ this.overlay.detach();
12746
12823
  }
12747
12824
  setValue(v) {
12748
12825
  this.value.set(v);
@@ -13110,26 +13187,166 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
13110
13187
  }], ctorParameters: () => [], propDecorators: { search: [{ type: i0.Input, args: [{ isSignal: true, alias: "search", required: true }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], minChars: [{ type: i0.Input, args: [{ isSignal: true, alias: "minChars", required: false }] }], debounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceMs", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], display: [{ type: i0.Input, args: [{ isSignal: true, alias: "display", required: false }] }], optionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionLabel", required: false }] }], optionValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionValue", required: false }] }], optionDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionDisabled", required: false }] }], selectedOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedOptions", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], searchingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchingText", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], searchErrorText: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchErrorText", required: false }] }], minCharsText: [{ type: i0.Input, args: [{ isSignal: true, alias: "minCharsText", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], removeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "removeLabel", required: false }] }], openLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "openLabel", required: false }] }], selected: [{ type: i0.Output, args: ["selected"] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], triggerEl: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], fieldEl: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], panelRef: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], optionTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsSelectOption), { isSignal: true }] }], emptyTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsSelectEmpty), { isSignal: true }] }], footerTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsSelectFooter), { isSignal: true }] }] } });
13111
13188
 
13112
13189
  const SDS_PHONE_COUNTRIES = [
13113
- { iso2: 'BE', name: 'Belgium', dialCode: '32', lengths: [9], format: '### ## ## ##', example: '470123456' },
13114
- { iso2: 'FR', name: 'France', dialCode: '33', lengths: [9], format: '# ## ## ## ##', example: '612345678' },
13115
- { iso2: 'LU', name: 'Luxembourg', dialCode: '352', lengths: [9, 8, 6], format: '### ### ###', example: '628123456' },
13116
- { iso2: 'NL', name: 'Netherlands', dialCode: '31', lengths: [9], format: '# ## ## ## ##', example: '612345678' },
13117
- { iso2: 'DE', name: 'Germany', dialCode: '49', lengths: [10, 11], format: '#### #######', example: '15123456789' },
13118
- { iso2: 'GB', name: 'United Kingdom', dialCode: '44', lengths: [10], format: '#### ######', example: '7400123456' },
13119
- { iso2: 'IE', name: 'Ireland', dialCode: '353', lengths: [9], format: '## ### ####', example: '850123456' },
13120
- { iso2: 'CH', name: 'Switzerland', dialCode: '41', lengths: [9], format: '## ### ## ##', example: '781234567' },
13121
- { iso2: 'ES', name: 'Spain', dialCode: '34', lengths: [9], format: '### ## ## ##', example: '612345678' },
13122
- { iso2: 'IT', name: 'Italy', dialCode: '39', lengths: [9, 10], format: '### ### ####', example: '3123456789' },
13123
- { iso2: 'PT', name: 'Portugal', dialCode: '351', lengths: [9], format: '### ### ###', example: '912345678' },
13124
- { iso2: 'AT', name: 'Austria', dialCode: '43', lengths: [10, 11], format: '### #######', example: '6641234567' },
13125
- { iso2: 'DK', name: 'Denmark', dialCode: '45', lengths: [8], format: '## ## ## ##', example: '20123456' },
13126
- { iso2: 'SE', name: 'Sweden', dialCode: '46', lengths: [9], format: '## ### ## ##', example: '701234567' },
13127
- { iso2: 'NO', name: 'Norway', dialCode: '47', lengths: [8], format: '### ## ###', example: '40612345' },
13128
- { iso2: 'PL', name: 'Poland', dialCode: '48', lengths: [9], format: '### ### ###', example: '512345678' },
13129
- { iso2: 'US', name: 'United States', dialCode: '1', lengths: [10], format: '(###) ###-####', example: '2015550123' },
13130
- { iso2: 'CA', name: 'Canada', dialCode: '1', lengths: [10], format: '(###) ###-####', example: '2042345678' },
13131
- { iso2: 'MA', name: 'Morocco', dialCode: '212', lengths: [9], format: '### ## ## ##', example: '612345678' },
13132
- { iso2: 'TR', name: 'Turkey', dialCode: '90', lengths: [10], format: '### ### ## ##', example: '5012345678' },
13190
+ {
13191
+ iso2: 'BE',
13192
+ name: 'Belgium',
13193
+ dialCode: '32',
13194
+ lengths: [9],
13195
+ format: '### ## ## ##',
13196
+ example: '470123456',
13197
+ },
13198
+ {
13199
+ iso2: 'FR',
13200
+ name: 'France',
13201
+ dialCode: '33',
13202
+ lengths: [9],
13203
+ format: '# ## ## ## ##',
13204
+ example: '612345678',
13205
+ },
13206
+ {
13207
+ iso2: 'LU',
13208
+ name: 'Luxembourg',
13209
+ dialCode: '352',
13210
+ lengths: [9, 8, 6],
13211
+ format: '### ### ###',
13212
+ example: '628123456',
13213
+ },
13214
+ {
13215
+ iso2: 'NL',
13216
+ name: 'Netherlands',
13217
+ dialCode: '31',
13218
+ lengths: [9],
13219
+ format: '# ## ## ## ##',
13220
+ example: '612345678',
13221
+ },
13222
+ {
13223
+ iso2: 'DE',
13224
+ name: 'Germany',
13225
+ dialCode: '49',
13226
+ lengths: [10, 11],
13227
+ format: '#### #######',
13228
+ example: '15123456789',
13229
+ },
13230
+ {
13231
+ iso2: 'GB',
13232
+ name: 'United Kingdom',
13233
+ dialCode: '44',
13234
+ lengths: [10],
13235
+ format: '#### ######',
13236
+ example: '7400123456',
13237
+ },
13238
+ {
13239
+ iso2: 'IE',
13240
+ name: 'Ireland',
13241
+ dialCode: '353',
13242
+ lengths: [9],
13243
+ format: '## ### ####',
13244
+ example: '850123456',
13245
+ },
13246
+ {
13247
+ iso2: 'CH',
13248
+ name: 'Switzerland',
13249
+ dialCode: '41',
13250
+ lengths: [9],
13251
+ format: '## ### ## ##',
13252
+ example: '781234567',
13253
+ },
13254
+ {
13255
+ iso2: 'ES',
13256
+ name: 'Spain',
13257
+ dialCode: '34',
13258
+ lengths: [9],
13259
+ format: '### ## ## ##',
13260
+ example: '612345678',
13261
+ },
13262
+ {
13263
+ iso2: 'IT',
13264
+ name: 'Italy',
13265
+ dialCode: '39',
13266
+ lengths: [9, 10],
13267
+ format: '### ### ####',
13268
+ example: '3123456789',
13269
+ },
13270
+ {
13271
+ iso2: 'PT',
13272
+ name: 'Portugal',
13273
+ dialCode: '351',
13274
+ lengths: [9],
13275
+ format: '### ### ###',
13276
+ example: '912345678',
13277
+ },
13278
+ {
13279
+ iso2: 'AT',
13280
+ name: 'Austria',
13281
+ dialCode: '43',
13282
+ lengths: [10, 11],
13283
+ format: '### #######',
13284
+ example: '6641234567',
13285
+ },
13286
+ {
13287
+ iso2: 'DK',
13288
+ name: 'Denmark',
13289
+ dialCode: '45',
13290
+ lengths: [8],
13291
+ format: '## ## ## ##',
13292
+ example: '20123456',
13293
+ },
13294
+ {
13295
+ iso2: 'SE',
13296
+ name: 'Sweden',
13297
+ dialCode: '46',
13298
+ lengths: [9],
13299
+ format: '## ### ## ##',
13300
+ example: '701234567',
13301
+ },
13302
+ {
13303
+ iso2: 'NO',
13304
+ name: 'Norway',
13305
+ dialCode: '47',
13306
+ lengths: [8],
13307
+ format: '### ## ###',
13308
+ example: '40612345',
13309
+ },
13310
+ {
13311
+ iso2: 'PL',
13312
+ name: 'Poland',
13313
+ dialCode: '48',
13314
+ lengths: [9],
13315
+ format: '### ### ###',
13316
+ example: '512345678',
13317
+ },
13318
+ {
13319
+ iso2: 'US',
13320
+ name: 'United States',
13321
+ dialCode: '1',
13322
+ lengths: [10],
13323
+ format: '(###) ###-####',
13324
+ example: '2015550123',
13325
+ },
13326
+ {
13327
+ iso2: 'CA',
13328
+ name: 'Canada',
13329
+ dialCode: '1',
13330
+ lengths: [10],
13331
+ format: '(###) ###-####',
13332
+ example: '2042345678',
13333
+ },
13334
+ {
13335
+ iso2: 'MA',
13336
+ name: 'Morocco',
13337
+ dialCode: '212',
13338
+ lengths: [9],
13339
+ format: '### ## ## ##',
13340
+ example: '612345678',
13341
+ },
13342
+ {
13343
+ iso2: 'TR',
13344
+ name: 'Turkey',
13345
+ dialCode: '90',
13346
+ lengths: [10],
13347
+ format: '### ### ## ##',
13348
+ example: '5012345678',
13349
+ },
13133
13350
  ];
13134
13351
  /** Reduce raw input to national-significant digits (drops non-digits & leading trunk zeros). */
13135
13352
  function sdsNormalizeSignificant(raw) {
@@ -13280,6 +13497,8 @@ class SdsPhoneInput {
13280
13497
  anchor = viewChild('anchor', ...(ngDevMode ? [{ debugName: "anchor" }] : []));
13281
13498
  field = viewChild('field', ...(ngDevMode ? [{ debugName: "field" }] : []));
13282
13499
  panelEl = null;
13500
+ /** Body-appending, positioning, follow-on-scroll and teardown — see SdsOverlayPanel. */
13501
+ overlay = new SdsOverlayPanel();
13283
13502
  isDisabled = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
13284
13503
  /** Countries after applying [onlyCountries] and [preferredCountries] ordering. */
13285
13504
  availableCountries = computed(() => {
@@ -13413,8 +13632,7 @@ class SdsPhoneInput {
13413
13632
  this.open.set(true);
13414
13633
  afterNextRender(() => {
13415
13634
  this.panelEl = this.host.nativeElement.querySelector('[data-sds-phone-panel]');
13416
- this.positionPanel();
13417
- this.addReflowListeners();
13635
+ this.attachPanel();
13418
13636
  this.panelEl?.querySelector('input')?.focus();
13419
13637
  this.scrollHighlightedIntoView();
13420
13638
  }, { injector: this.injector });
@@ -13422,8 +13640,7 @@ class SdsPhoneInput {
13422
13640
  close(returnFocus = false) {
13423
13641
  if (!this.open())
13424
13642
  return;
13425
- this.removeReflowListeners();
13426
- this.panelEl?.remove();
13643
+ this.overlay.detach();
13427
13644
  this.panelEl = null;
13428
13645
  this.open.set(false);
13429
13646
  if (returnFocus)
@@ -13444,7 +13661,7 @@ class SdsPhoneInput {
13444
13661
  onFilter(event) {
13445
13662
  this.filterText.set(event.target.value);
13446
13663
  this.highlightedIndex.set(this.filteredCountries().length ? 0 : -1);
13447
- setTimeout(() => this.positionPanel());
13664
+ setTimeout(() => this.overlay.reposition());
13448
13665
  }
13449
13666
  // --- keyboard ---
13450
13667
  onTriggerKeydown(event) {
@@ -13514,46 +13731,12 @@ class SdsPhoneInput {
13514
13731
  ?.scrollIntoView({ block: 'nearest' });
13515
13732
  });
13516
13733
  }
13517
- // --- body-appended overlay positioning (mirrors sds-select) ---
13518
- reflow = () => this.positionPanel();
13519
- listening = false;
13520
- addReflowListeners() {
13521
- if (this.listening)
13522
- return;
13523
- window.addEventListener('scroll', this.reflow, true);
13524
- window.addEventListener('resize', this.reflow);
13525
- this.listening = true;
13526
- }
13527
- removeReflowListeners() {
13528
- if (!this.listening)
13529
- return;
13530
- window.removeEventListener('scroll', this.reflow, true);
13531
- window.removeEventListener('resize', this.reflow);
13532
- this.listening = false;
13533
- }
13534
- positionPanel() {
13535
- const anchorEl = this.anchor()?.nativeElement;
13536
- const panelEl = this.panelEl;
13537
- if (!anchorEl || !panelEl)
13538
- return;
13539
- if (panelEl.parentElement !== document.body)
13540
- document.body.appendChild(panelEl);
13541
- const r = anchorEl.getBoundingClientRect();
13542
- panelEl.style.minWidth = `${Math.round(r.width)}px`;
13543
- const ph = panelEl.offsetHeight || 280;
13544
- const gap = 4;
13545
- const margin = 8;
13546
- let top = r.bottom + gap;
13547
- if (top + ph > window.innerHeight - margin && r.top - gap - ph > margin) {
13548
- top = r.top - gap - ph;
13549
- }
13550
- let left = r.left;
13551
- const pw = panelEl.offsetWidth || r.width;
13552
- if (left + pw > window.innerWidth - margin) {
13553
- left = Math.max(margin, window.innerWidth - margin - pw);
13734
+ /** Attach once the panel exists — it is rendered inside an `@if`. */
13735
+ attachPanel() {
13736
+ const trigger = this.anchor()?.nativeElement;
13737
+ if (trigger && this.panelEl) {
13738
+ this.overlay.attach(trigger, this.panelEl, { matchTriggerWidth: true });
13554
13739
  }
13555
- panelEl.style.top = `${Math.round(top)}px`;
13556
- panelEl.style.left = `${Math.round(left)}px`;
13557
13740
  }
13558
13741
  onDocumentClick(event) {
13559
13742
  if (!this.open())
@@ -13569,8 +13752,7 @@ class SdsPhoneInput {
13569
13752
  this.onTouched();
13570
13753
  }
13571
13754
  ngOnDestroy() {
13572
- this.removeReflowListeners();
13573
- this.panelEl?.remove();
13755
+ this.overlay.detach();
13574
13756
  }
13575
13757
  emit() {
13576
13758
  const value = this.e164();
@@ -13667,7 +13849,9 @@ class SdsPhoneInput {
13667
13849
  (click)="toggle()"
13668
13850
  (keydown)="onTriggerKeydown($event)"
13669
13851
  >
13670
- <span class="grid h-5 min-w-7 place-items-center rounded bg-muted px-1 text-[11px] font-semibold tracking-wide text-ink-soft">
13852
+ <span
13853
+ class="grid h-5 min-w-7 place-items-center rounded bg-muted px-1 text-[11px] font-semibold tracking-wide text-ink-soft"
13854
+ >
13671
13855
  {{ country().iso2 }}
13672
13856
  </span>
13673
13857
  <span class="text-ink-soft">+{{ country().dialCode }}</span>
@@ -13718,8 +13902,14 @@ class SdsPhoneInput {
13718
13902
  class="fixed z-[1000] overflow-hidden rounded-card border border-border bg-surface shadow-lg"
13719
13903
  >
13720
13904
  <div class="border-b border-border p-2">
13721
- <div class="relative flex items-center rounded-control border border-border focus-within:border-brand-500">
13722
- <sds-icon name="search" [size]="16" class="pointer-events-none absolute left-2.5 text-ink-faint" />
13905
+ <div
13906
+ class="relative flex items-center rounded-control border border-border focus-within:border-brand-500"
13907
+ >
13908
+ <sds-icon
13909
+ name="search"
13910
+ [size]="16"
13911
+ class="pointer-events-none absolute left-2.5 text-ink-faint"
13912
+ />
13723
13913
  <input
13724
13914
  type="text"
13725
13915
  [attr.aria-controls]="listId"
@@ -13744,7 +13934,9 @@ class SdsPhoneInput {
13744
13934
  (click)="selectCountry(c)"
13745
13935
  (mouseenter)="highlightedIndex.set(i)"
13746
13936
  >
13747
- <span class="grid h-5 min-w-7 place-items-center rounded bg-muted px-1 text-[11px] font-semibold tracking-wide text-ink-soft">
13937
+ <span
13938
+ class="grid h-5 min-w-7 place-items-center rounded bg-muted px-1 text-[11px] font-semibold tracking-wide text-ink-soft"
13939
+ >
13748
13940
  {{ c.iso2 }}
13749
13941
  </span>
13750
13942
  <span class="flex-1 truncate">{{ countryName(c) }}</span>
@@ -13808,7 +14000,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
13808
14000
  (click)="toggle()"
13809
14001
  (keydown)="onTriggerKeydown($event)"
13810
14002
  >
13811
- <span class="grid h-5 min-w-7 place-items-center rounded bg-muted px-1 text-[11px] font-semibold tracking-wide text-ink-soft">
14003
+ <span
14004
+ class="grid h-5 min-w-7 place-items-center rounded bg-muted px-1 text-[11px] font-semibold tracking-wide text-ink-soft"
14005
+ >
13812
14006
  {{ country().iso2 }}
13813
14007
  </span>
13814
14008
  <span class="text-ink-soft">+{{ country().dialCode }}</span>
@@ -13859,8 +14053,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
13859
14053
  class="fixed z-[1000] overflow-hidden rounded-card border border-border bg-surface shadow-lg"
13860
14054
  >
13861
14055
  <div class="border-b border-border p-2">
13862
- <div class="relative flex items-center rounded-control border border-border focus-within:border-brand-500">
13863
- <sds-icon name="search" [size]="16" class="pointer-events-none absolute left-2.5 text-ink-faint" />
14056
+ <div
14057
+ class="relative flex items-center rounded-control border border-border focus-within:border-brand-500"
14058
+ >
14059
+ <sds-icon
14060
+ name="search"
14061
+ [size]="16"
14062
+ class="pointer-events-none absolute left-2.5 text-ink-faint"
14063
+ />
13864
14064
  <input
13865
14065
  type="text"
13866
14066
  [attr.aria-controls]="listId"
@@ -13885,7 +14085,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
13885
14085
  (click)="selectCountry(c)"
13886
14086
  (mouseenter)="highlightedIndex.set(i)"
13887
14087
  >
13888
- <span class="grid h-5 min-w-7 place-items-center rounded bg-muted px-1 text-[11px] font-semibold tracking-wide text-ink-soft">
14088
+ <span
14089
+ class="grid h-5 min-w-7 place-items-center rounded bg-muted px-1 text-[11px] font-semibold tracking-wide text-ink-soft"
14090
+ >
13889
14091
  {{ c.iso2 }}
13890
14092
  </span>
13891
14093
  <span class="flex-1 truncate">{{ countryName(c) }}</span>
@@ -21651,5 +21853,5 @@ function provideSdsCharts(config) {
21651
21853
  * Generated bundle index. Do not edit.
21652
21854
  */
21653
21855
 
21654
- export { SDS_ALLERGENS, SDS_ALLERGEN_ICONS, SDS_ALLERGEN_LABELS_EN, SDS_CHART_CONFIG, SDS_CHART_LABELS_EN, SDS_ICONS, SDS_PHONE_COUNTRIES, SDS_THEME_STORAGE_KEY, SDS_UI_PREF_LANG_PARAM, SDS_UI_PREF_THEME_PARAM, SdsAccordion, SdsAccordionContent, SdsAccordionHeader, SdsAccordionPanel, SdsAccordionStore, SdsAddressAutocomplete, SdsAddressExtra, SdsAllergenIcon, SdsAutocomplete, SdsAvatar, SdsBadge, SdsBulkBar, SdsButton, SdsCalendar, SdsCard, SdsCheckbox, SdsChecklistRow, SdsChip, SdsChipGroup, SdsDatePicker, SdsDateRangePicker, SdsEnvBanner, SdsFileDrop, SdsFormActions, SdsIcon, SdsImageCropper, SdsInput, SdsInputNumber, SdsLanguageSwitcher, SdsLocalizedInput, SdsMenu, SdsMenuItem, SdsMenuTrigger, SdsMultiSelect, SdsNavGroup, SdsNavItem, SdsOtpInput, SdsPaginator, SdsPhoneInput, SdsRadio, SdsRangeCalendar, SdsScore, SdsSearchCommand, SdsSegmented, SdsSelect, SdsSelectEmpty, SdsSelectFooter, SdsSelectOption, SdsSeverity, SdsShell, SdsSidebar, SdsSortableHandle, SdsSortableItem, SdsSortableList, SdsStatusTile, SdsTab, SdsTabList, SdsTabPanel, SdsTabPanels, SdsTable, SdsTableCell, SdsTableEmpty, SdsTableHeader, SdsTableRowExpansion, SdsTabs, SdsTabsStore, SdsTextarea, SdsThemeService, SdsThemeToggle, SdsTimeInput, SdsToastHost, SdsToastService, SdsToggleSwitch, SdsTopbar, SdsTriStateChip, SdsUserMenu, SidebarStore, addDays, addMonths, addMonthsKeepDay, appendUiPrefs, diffInDays, formatDateRange, formatDdMmYyyy, formatHhMm, isOutOfRange, isSameDay, isSameRange, isSdsThemeMode, isoDay, pad2, provideSdsCharts, readUiPrefs, sdsFileMatchesAccept, sdsFormatBytes, sdsFormatNational, sdsLastDays, sdsLastMonths, sdsMatchDialCode, sdsNormalizeSignificant, startOfDay, startOfMonth, stripUiPrefs };
21856
+ export { SDS_ALLERGENS, SDS_ALLERGEN_ICONS, SDS_ALLERGEN_LABELS_EN, SDS_CHART_CONFIG, SDS_CHART_LABELS_EN, SDS_ICONS, SDS_PHONE_COUNTRIES, SDS_THEME_STORAGE_KEY, SDS_UI_PREF_LANG_PARAM, SDS_UI_PREF_THEME_PARAM, SdsAccordion, SdsAccordionContent, SdsAccordionHeader, SdsAccordionPanel, SdsAccordionStore, SdsAddressAutocomplete, SdsAddressExtra, SdsAllergenIcon, SdsAutocomplete, SdsAvatar, SdsBadge, SdsBulkBar, SdsButton, SdsCalendar, SdsCard, SdsCheckbox, SdsChecklistRow, SdsChip, SdsChipGroup, SdsDatePicker, SdsDateRangePicker, SdsEnvBanner, SdsFileDrop, SdsFormActions, SdsIcon, SdsImageCropper, SdsInput, SdsInputNumber, SdsLanguageSwitcher, SdsLocalizedInput, SdsMenu, SdsMenuItem, SdsMenuTrigger, SdsMultiSelect, SdsNavGroup, SdsNavItem, SdsOtpInput, SdsOverlayPanel, SdsPaginator, SdsPhoneInput, SdsRadio, SdsRangeCalendar, SdsScore, SdsSearchCommand, SdsSegmented, SdsSelect, SdsSelectEmpty, SdsSelectFooter, SdsSelectOption, SdsSeverity, SdsShell, SdsSidebar, SdsSortableHandle, SdsSortableItem, SdsSortableList, SdsStatusTile, SdsTab, SdsTabList, SdsTabPanel, SdsTabPanels, SdsTable, SdsTableCell, SdsTableEmpty, SdsTableHeader, SdsTableRowExpansion, SdsTabs, SdsTabsStore, SdsTextarea, SdsThemeService, SdsThemeToggle, SdsTimeInput, SdsToastHost, SdsToastService, SdsToggleSwitch, SdsTopbar, SdsTriStateChip, SdsUserMenu, SidebarStore, addDays, addMonths, addMonthsKeepDay, appendUiPrefs, diffInDays, formatDateRange, formatDdMmYyyy, formatHhMm, isOutOfRange, isSameDay, isSameRange, isSdsThemeMode, isoDay, pad2, placePanel, provideSdsCharts, readUiPrefs, sdsFileMatchesAccept, sdsFormatBytes, sdsFormatNational, sdsLastDays, sdsLastMonths, sdsMatchDialCode, sdsNormalizeSignificant, startOfDay, startOfMonth, stripUiPrefs };
21655
21857
  //# sourceMappingURL=besaitech-ng-design-system.mjs.map