@besaitech/ng-design-system 0.0.80 → 0.0.82

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.
@@ -5826,6 +5826,127 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
5826
5826
  }]
5827
5827
  }], propDecorators: { level: [{ type: i0.Input, args: [{ isSignal: true, alias: "level", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
5828
5828
 
5829
+ /**
5830
+ * Where a floating panel goes, and the plumbing that keeps it there.
5831
+ *
5832
+ * <p><b>Why this exists.</b> Ten components in this library append a panel to `<body>` and position
5833
+ * it against their trigger, and nine of them had their own copy of the same twenty-odd lines. The
5834
+ * copies had already drifted: some clamp the panel inside the viewport vertically and some only
5835
+ * flip it, some match the trigger's width and some do not. Worse, the *lifecycle* was duplicated
5836
+ * too — and `sds-autocomplete` shipped a version that removed the panel through a `viewChild`
5837
+ * already cleared by `ngOnDestroy`, orphaning it in `<body>` for ever. That bug could only exist in
5838
+ * one copy of nine.
5839
+ */
5840
+ /**
5841
+ * The arithmetic, with no DOM in it.
5842
+ *
5843
+ * <p>Vertical: below the trigger, flipped above when it does not fit below and fits better above —
5844
+ * then <b>clamped inside the viewport</b>. The clamp is not decoration: without it a panel taller
5845
+ * than the space either side is pushed off the top edge and its first rows become unreachable, and
5846
+ * four of the nine copies were missing it.
5847
+ *
5848
+ * <p>Horizontal: the panel's start (or end) edge meets the trigger's, then clamped the same way. A
5849
+ * panel wider than the viewport pins to the left margin rather than running off both sides.
5850
+ */
5851
+ function placePanel(trigger, panel, viewport, options = {}) {
5852
+ const gap = options.gap ?? 4;
5853
+ const margin = options.margin ?? 8;
5854
+ const roomBelow = viewport.height - trigger.bottom - gap;
5855
+ const roomAbove = trigger.top - gap;
5856
+ const above = options.prefer === 'above'
5857
+ ? roomAbove >= panel.height || roomAbove > roomBelow
5858
+ : roomBelow < panel.height && roomAbove > roomBelow;
5859
+ const top = clamp(above ? trigger.top - gap - panel.height : trigger.bottom + gap, margin, viewport.height - margin - panel.height);
5860
+ const left = clamp(options.align === 'end' ? trigger.right - panel.width : trigger.left, margin, viewport.width - margin - panel.width);
5861
+ return { top: Math.round(top), left: Math.round(left) };
5862
+ }
5863
+ /**
5864
+ * A viewport bound that may be smaller than the lower one — when the panel is taller than the
5865
+ * window there is no position that satisfies both, and the top edge is the one that must win, or
5866
+ * the panel starts off-screen and cannot be scrolled back.
5867
+ */
5868
+ function clamp(value, low, high) {
5869
+ return Math.max(low, Math.min(value, Math.max(low, high)));
5870
+ }
5871
+ /**
5872
+ * The DOM half: append to `<body>`, place, follow scroll and resize, and — the part that was worth
5873
+ * centralising — take it all down again exactly once.
5874
+ *
5875
+ * <p>Hold one of these per component and call {@link detach} from `ngOnDestroy`. It keeps its own
5876
+ * reference to the panel element, so it does not depend on a `viewChild` that Angular has already
5877
+ * cleared by the time the component is torn down.
5878
+ */
5879
+ class SdsOverlayPanel {
5880
+ trigger = null;
5881
+ panel = null;
5882
+ options = {};
5883
+ listening = false;
5884
+ reflow = () => this.reposition();
5885
+ /** Attach (or re-attach) the panel to the body and start following the trigger. */
5886
+ attach(trigger, panel, options = {}) {
5887
+ this.trigger = trigger;
5888
+ this.panel = panel;
5889
+ this.options = options;
5890
+ if (panel.parentElement !== document.body) {
5891
+ document.body.appendChild(panel);
5892
+ }
5893
+ this.reposition();
5894
+ if (!this.listening) {
5895
+ // `true` for scroll: a panel inside a scrolling container must follow it, and scroll does
5896
+ // not bubble.
5897
+ window.addEventListener('scroll', this.reflow, true);
5898
+ window.addEventListener('resize', this.reflow);
5899
+ this.listening = true;
5900
+ }
5901
+ }
5902
+ reposition() {
5903
+ const trigger = this.trigger;
5904
+ const panel = this.panel;
5905
+ if (!trigger || !panel) {
5906
+ return;
5907
+ }
5908
+ const r = trigger.getBoundingClientRect();
5909
+ // Before measuring, because the width we are about to impose is the width placePanel must see.
5910
+ if (this.options.width === 'min') {
5911
+ panel.style.minWidth = `${Math.round(r.width)}px`;
5912
+ }
5913
+ else if (this.options.width === 'exact') {
5914
+ panel.style.width = `${Math.round(r.width)}px`;
5915
+ }
5916
+ const { top, left } = placePanel(r,
5917
+ // offset* is 0 before the panel has laid out; the fallbacks keep the first frame sane.
5918
+ { width: panel.offsetWidth || r.width, height: panel.offsetHeight || 240 }, { width: window.innerWidth, height: window.innerHeight }, this.options);
5919
+ panel.style.top = `${top}px`;
5920
+ panel.style.left = `${left}px`;
5921
+ }
5922
+ /**
5923
+ * The live panel element, or null while closed.
5924
+ *
5925
+ * <p>Read-only on purpose. A component that needs to reach inside the panel — to focus a row, to
5926
+ * trap Tab, to read its options — asks here rather than keeping its own field. A second
5927
+ * reference is how the autocomplete came to remove the panel through a `viewChild` that Angular
5928
+ * had already cleared, which is the bug this class exists to make unrepeatable.
5929
+ */
5930
+ get element() {
5931
+ return this.panel;
5932
+ }
5933
+ /** Does the panel contain this node? For "did the click land outside" checks. */
5934
+ contains(node) {
5935
+ return !!node && !!this.panel?.contains(node);
5936
+ }
5937
+ /** Stop following, and remove the panel from the body. Safe to call more than once. */
5938
+ detach() {
5939
+ if (this.listening) {
5940
+ window.removeEventListener('scroll', this.reflow, true);
5941
+ window.removeEventListener('resize', this.reflow);
5942
+ this.listening = false;
5943
+ }
5944
+ this.panel?.remove();
5945
+ this.panel = null;
5946
+ this.trigger = null;
5947
+ }
5948
+ }
5949
+
5829
5950
  let nextId$f = 0;
5830
5951
  /**
5831
5952
  * Marks the element inside `<sds-menu>` that opens it. It may be a native button or a component that
@@ -5896,7 +6017,8 @@ class SdsMenu {
5896
6017
  /** An action was activated. */
5897
6018
  select = output();
5898
6019
  projectedTrigger = contentChild(SdsMenuTrigger, ...(ngDevMode ? [{ debugName: "projectedTrigger" }] : []));
5899
- panelEl = null;
6020
+ /** Body-appending, positioning, follow-on-scroll and teardown — see SdsOverlayPanel. */
6021
+ overlay = new SdsOverlayPanel();
5900
6022
  /** Which item gets focus when the panel opens: 'first' | 'last' | 'none'. */
5901
6023
  focusOnOpen = 'first';
5902
6024
  typeBuffer = '';
@@ -5991,23 +6113,18 @@ class SdsMenu {
5991
6113
  afterOpen() {
5992
6114
  if (!this.open())
5993
6115
  return;
5994
- this.panelEl = this.host.nativeElement.querySelector('[data-sds-menu-panel]');
5995
- if (!this.panelEl)
6116
+ const panel = this.host.nativeElement.querySelector('[data-sds-menu-panel]');
6117
+ const trigger = this.triggerEl();
6118
+ if (!panel || !trigger)
5996
6119
  return;
5997
- if (this.panelEl.parentElement !== document.body)
5998
- document.body.appendChild(this.panelEl);
5999
6120
  // Name the menu after its trigger unless an explicit [ariaLabel] was given.
6000
- const trigger = this.triggerEl();
6001
- if (!this.ariaLabel() && trigger?.id)
6002
- this.panelEl.setAttribute('aria-labelledby', trigger.id);
6003
- this.position();
6004
- this.addReflowListeners();
6121
+ if (!this.ariaLabel() && trigger.id)
6122
+ panel.setAttribute('aria-labelledby', trigger.id);
6123
+ this.overlay.attach(trigger, panel, { align: this.align() });
6005
6124
  this.focusItemAt(this.focusOnOpen);
6006
6125
  }
6007
6126
  teardown() {
6008
- this.removeReflowListeners();
6009
- this.panelEl?.remove();
6010
- this.panelEl = null;
6127
+ this.overlay.detach();
6011
6128
  }
6012
6129
  // --- trigger events -------------------------------------------------------
6013
6130
  onTriggerClick(event) {
@@ -6094,12 +6211,12 @@ class SdsMenu {
6094
6211
  this.typeTimer = setTimeout(() => (this.typeBuffer = ''), 600);
6095
6212
  }
6096
6213
  focusIndex(i) {
6097
- this.panelEl?.querySelector(`[data-index="${i}"]`)?.focus();
6214
+ this.overlay.element?.querySelector(`[data-index="${i}"]`)?.focus();
6098
6215
  }
6099
6216
  focusItemAt(where) {
6100
6217
  const idx = this.enabledIndexes();
6101
6218
  if (!idx.length) {
6102
- this.panelEl?.focus();
6219
+ this.overlay.element?.focus();
6103
6220
  return;
6104
6221
  }
6105
6222
  this.focusIndex(where === 'first' ? idx[0] : idx[idx.length - 1]);
@@ -6119,47 +6236,10 @@ class SdsMenu {
6119
6236
  const target = event.target;
6120
6237
  if (this.host.nativeElement.contains(target))
6121
6238
  return;
6122
- if (this.panelEl?.contains(target))
6239
+ if (this.overlay.contains(target))
6123
6240
  return;
6124
6241
  this.close(false);
6125
6242
  }
6126
- // --- positioning (body-appended, mirrors sds-select) ------------------------
6127
- reflow = () => this.position();
6128
- listening = false;
6129
- addReflowListeners() {
6130
- if (this.listening)
6131
- return;
6132
- window.addEventListener('scroll', this.reflow, true);
6133
- window.addEventListener('resize', this.reflow);
6134
- this.listening = true;
6135
- }
6136
- removeReflowListeners() {
6137
- if (!this.listening)
6138
- return;
6139
- window.removeEventListener('scroll', this.reflow, true);
6140
- window.removeEventListener('resize', this.reflow);
6141
- this.listening = false;
6142
- }
6143
- position() {
6144
- const trigger = this.triggerEl();
6145
- const panel = this.panelEl;
6146
- if (!trigger || !panel)
6147
- return;
6148
- const r = trigger.getBoundingClientRect();
6149
- const pw = panel.offsetWidth;
6150
- const ph = panel.offsetHeight;
6151
- const gap = 4;
6152
- const margin = 8;
6153
- let top = r.bottom + gap;
6154
- if (top + ph > window.innerHeight - margin && r.top - gap - ph > margin) {
6155
- top = r.top - gap - ph;
6156
- }
6157
- top = Math.max(margin, Math.min(top, window.innerHeight - margin - ph));
6158
- let left = this.align() === 'end' ? r.right - pw : r.left;
6159
- left = Math.max(margin, Math.min(left, window.innerWidth - margin - pw));
6160
- panel.style.top = `${Math.round(top)}px`;
6161
- panel.style.left = `${Math.round(left)}px`;
6162
- }
6163
6243
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
6164
6244
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsMenu, isStandalone: true, selector: "sds-menu", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, triggerLabel: { classPropertyName: "triggerLabel", publicName: "triggerLabel", isSignal: true, isRequired: false, transformFunction: null }, triggerIcon: { classPropertyName: "triggerIcon", publicName: "triggerIcon", isSignal: true, isRequired: false, transformFunction: null }, triggerText: { classPropertyName: "triggerText", publicName: "triggerText", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, keepOpen: { classPropertyName: "keepOpen", publicName: "keepOpen", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", select: "select" }, host: { listeners: { "document:click": "onDocumentClick($event)" }, classAttribute: "relative inline-flex" }, queries: [{ propertyName: "projectedTrigger", first: true, predicate: SdsMenuTrigger, descendants: true, isSignal: true }], ngImport: i0, template: `
6165
6245
  <span
@@ -9105,8 +9185,8 @@ class SdsDatePicker {
9105
9185
  cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : []));
9106
9186
  calendar = viewChild(SdsCalendar, ...(ngDevMode ? [{ debugName: "calendar" }] : []));
9107
9187
  trigger = viewChild('trigger', ...(ngDevMode ? [{ debugName: "trigger" }] : []));
9108
- /** The overlay panel, relocated to document.body while open. */
9109
- panelEl = null;
9188
+ /** Body-appending, positioning, follow-on-scroll and teardown — see SdsOverlayPanel. */
9189
+ overlay = new SdsOverlayPanel();
9110
9190
  isDisabled = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
9111
9191
  displayValue = computed(() => {
9112
9192
  const v = this.value();
@@ -9140,18 +9220,19 @@ class SdsDatePicker {
9140
9220
  this.onTouched();
9141
9221
  // Run after the overlay is rendered, then relocate it to <body>.
9142
9222
  afterNextRender(() => {
9143
- this.panelEl = this.host.nativeElement.querySelector('[role="dialog"]');
9144
- this.positionPanel(); // also relocates to document.body
9145
- this.addReflowListeners();
9223
+ const panel = this.host.nativeElement.querySelector('[role="dialog"]');
9224
+ const trigger = this.trigger()?.nativeElement;
9225
+ if (panel && trigger) {
9226
+ // gap 6, as this field has always used — the shared default is 4.
9227
+ this.overlay.attach(trigger, panel, { gap: 6 });
9228
+ }
9146
9229
  this.calendar()?.focusActiveCell();
9147
9230
  }, { injector: this.injector });
9148
9231
  }
9149
9232
  close(returnFocus = false) {
9150
9233
  if (!this.open())
9151
9234
  return;
9152
- this.removeReflowListeners();
9153
- this.panelEl?.remove(); // pull the body-relocated node before destroying the view
9154
- this.panelEl = null;
9235
+ this.overlay.detach(); // pull the body-relocated node before destroying the view
9155
9236
  this.open.set(false);
9156
9237
  if (returnFocus) {
9157
9238
  this.host.nativeElement.querySelector('input')?.focus();
@@ -9196,17 +9277,18 @@ class SdsDatePicker {
9196
9277
  * the page behind the dialog. Keeps Tab / Shift+Tab cycling within the panel's own focusables.
9197
9278
  */
9198
9279
  onPanelKeydown(event) {
9199
- if (event.key !== 'Tab' || !this.panelEl)
9280
+ const panel = this.overlay.element;
9281
+ if (event.key !== 'Tab' || !panel)
9200
9282
  return;
9201
9283
  const sel = 'a[href],button:not([disabled]):not([tabindex="-1"]),input:not([disabled]),' +
9202
9284
  '[tabindex]:not([tabindex="-1"])';
9203
- const items = Array.from(this.panelEl.querySelectorAll(sel)).filter((el) => el.offsetParent !== null);
9285
+ const items = Array.from(panel.querySelectorAll(sel)).filter((el) => el.offsetParent !== null);
9204
9286
  if (items.length === 0)
9205
9287
  return;
9206
9288
  const first = items[0];
9207
9289
  const last = items[items.length - 1];
9208
9290
  const active = document.activeElement;
9209
- if (event.shiftKey && (active === first || !this.panelEl.contains(active))) {
9291
+ if (event.shiftKey && (active === first || !panel.contains(active))) {
9210
9292
  event.preventDefault();
9211
9293
  last.focus();
9212
9294
  }
@@ -9220,59 +9302,12 @@ class SdsDatePicker {
9220
9302
  return;
9221
9303
  const target = event.target;
9222
9304
  const inField = this.host.nativeElement.contains(target);
9223
- const inPanel = !!this.panelEl && this.panelEl.contains(target);
9305
+ const inPanel = this.overlay.contains(target);
9224
9306
  if (!inField && !inPanel)
9225
9307
  this.close();
9226
9308
  }
9227
- // --- body-appended overlay positioning ---
9228
- reflow = () => this.positionPanel();
9229
- listening = false;
9230
- addReflowListeners() {
9231
- if (this.listening)
9232
- return;
9233
- // capture=true so scrolling any ancestor scroll container repositions.
9234
- window.addEventListener('scroll', this.reflow, true);
9235
- window.addEventListener('resize', this.reflow);
9236
- this.listening = true;
9237
- }
9238
- removeReflowListeners() {
9239
- if (!this.listening)
9240
- return;
9241
- window.removeEventListener('scroll', this.reflow, true);
9242
- window.removeEventListener('resize', this.reflow);
9243
- this.listening = false;
9244
- }
9245
- positionPanel() {
9246
- const triggerEl = this.trigger()?.nativeElement;
9247
- const panelEl = this.panelEl;
9248
- if (!triggerEl || !panelEl)
9249
- return;
9250
- // appendTo body: relocate out of any clipping/stacking ancestor.
9251
- if (panelEl.parentElement !== document.body) {
9252
- document.body.appendChild(panelEl);
9253
- }
9254
- const r = triggerEl.getBoundingClientRect();
9255
- const ph = panelEl.offsetHeight || 360;
9256
- const pw = panelEl.offsetWidth || 296;
9257
- const gap = 6;
9258
- const margin = 8;
9259
- // Below the field by default; flip above when there isn't room.
9260
- let top = r.bottom + gap;
9261
- if (top + ph > window.innerHeight - margin && r.top - gap - ph > margin) {
9262
- top = r.top - gap - ph;
9263
- }
9264
- // Align to the field's left, but keep it within the viewport.
9265
- let left = r.left;
9266
- if (left + pw > window.innerWidth - margin) {
9267
- left = Math.max(margin, window.innerWidth - margin - pw);
9268
- }
9269
- // Set directly (position: fixed → viewport coordinates) to avoid a flash.
9270
- panelEl.style.top = `${Math.round(top)}px`;
9271
- panelEl.style.left = `${Math.round(left)}px`;
9272
- }
9273
9309
  ngOnDestroy() {
9274
- this.removeReflowListeners();
9275
- this.panelEl?.remove();
9310
+ this.overlay.detach();
9276
9311
  }
9277
9312
  setValue(v) {
9278
9313
  this.value.set(v);
@@ -9379,7 +9414,7 @@ class SdsDatePicker {
9379
9414
 
9380
9415
  @if (open()) {
9381
9416
  <!-- Relocated to document.body on open (appendTo body) to escape
9382
- ancestor overflow/stacking; positioned via positionPanel(). -->
9417
+ ancestor overflow/stacking; placed by SdsOverlayPanel. -->
9383
9418
  <div
9384
9419
  class="fixed z-[1000]"
9385
9420
  role="dialog"
@@ -9514,7 +9549,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
9514
9549
 
9515
9550
  @if (open()) {
9516
9551
  <!-- Relocated to document.body on open (appendTo body) to escape
9517
- ancestor overflow/stacking; positioned via positionPanel(). -->
9552
+ ancestor overflow/stacking; placed by SdsOverlayPanel. -->
9518
9553
  <div
9519
9554
  class="fixed z-[1000]"
9520
9555
  role="dialog"
@@ -9943,7 +9978,12 @@ class SdsRangeCalendar {
9943
9978
  }
9944
9979
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsRangeCalendar, deps: [], target: i0.ɵɵFactoryTarget.Component });
9945
9980
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsRangeCalendar, isStandalone: true, selector: "sds-range-calendar", inputs: { start: { classPropertyName: "start", publicName: "start", isSignal: true, isRequired: false, transformFunction: null }, end: { classPropertyName: "end", publicName: "end", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, maxDays: { classPropertyName: "maxDays", publicName: "maxDays", isSignal: true, isRequired: false, transformFunction: null }, firstDayOfWeek: { classPropertyName: "firstDayOfWeek", publicName: "firstDayOfWeek", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, months: { classPropertyName: "months", publicName: "months", isSignal: true, isRequired: false, transformFunction: null }, calendarLabel: { classPropertyName: "calendarLabel", publicName: "calendarLabel", isSignal: true, isRequired: false, transformFunction: null }, previousLabel: { classPropertyName: "previousLabel", publicName: "previousLabel", isSignal: true, isRequired: false, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { start: "startChange", end: "endChange", daySelect: "daySelect", rangeSelect: "rangeSelect", dayHover: "dayHover" }, host: { classAttribute: "block" }, ngImport: i0, template: `
9946
- <div class="select-none" role="group" [attr.aria-label]="calendarLabel()" (mouseleave)="setHover(null)">
9981
+ <div
9982
+ class="select-none"
9983
+ role="group"
9984
+ [attr.aria-label]="calendarLabel()"
9985
+ (mouseleave)="setHover(null)"
9986
+ >
9947
9987
  <div class="flex flex-col gap-4 sm:flex-row sm:gap-6">
9948
9988
  @for (m of monthViews(); track m.key; let first = $first; let last = $last) {
9949
9989
  <div class="w-[17.5rem]">
@@ -9979,7 +10019,11 @@ class SdsRangeCalendar {
9979
10019
  }
9980
10020
  </div>
9981
10021
 
9982
- <table class="w-full table-fixed border-collapse" role="grid" [attr.aria-label]="m.title">
10022
+ <table
10023
+ class="w-full table-fixed border-collapse"
10024
+ role="grid"
10025
+ [attr.aria-label]="m.title"
10026
+ >
9983
10027
  <thead>
9984
10028
  <tr>
9985
10029
  @for (wd of weekdayLabels(); track $index) {
@@ -10036,7 +10080,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
10036
10080
  imports: [SdsIcon],
10037
10081
  host: { class: 'block' },
10038
10082
  template: `
10039
- <div class="select-none" role="group" [attr.aria-label]="calendarLabel()" (mouseleave)="setHover(null)">
10083
+ <div
10084
+ class="select-none"
10085
+ role="group"
10086
+ [attr.aria-label]="calendarLabel()"
10087
+ (mouseleave)="setHover(null)"
10088
+ >
10040
10089
  <div class="flex flex-col gap-4 sm:flex-row sm:gap-6">
10041
10090
  @for (m of monthViews(); track m.key; let first = $first; let last = $last) {
10042
10091
  <div class="w-[17.5rem]">
@@ -10072,7 +10121,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
10072
10121
  }
10073
10122
  </div>
10074
10123
 
10075
- <table class="w-full table-fixed border-collapse" role="grid" [attr.aria-label]="m.title">
10124
+ <table
10125
+ class="w-full table-fixed border-collapse"
10126
+ role="grid"
10127
+ [attr.aria-label]="m.title"
10128
+ >
10076
10129
  <thead>
10077
10130
  <tr>
10078
10131
  @for (wd of weekdayLabels(); track $index) {
@@ -10220,8 +10273,8 @@ class SdsDateRangePicker {
10220
10273
  pendingEnd = signal(null, ...(ngDevMode ? [{ debugName: "pendingEnd" }] : []));
10221
10274
  calendar = viewChild(SdsRangeCalendar, ...(ngDevMode ? [{ debugName: "calendar" }] : []));
10222
10275
  trigger = viewChild('trigger', ...(ngDevMode ? [{ debugName: "trigger" }] : []));
10223
- /** The overlay panel, relocated to document.body while open. */
10224
- panelEl = null;
10276
+ /** Body-appending, positioning, follow-on-scroll and teardown — see SdsOverlayPanel. */
10277
+ overlay = new SdsOverlayPanel();
10225
10278
  /** Id of the preset the current value equals, or `null` (no value, or a custom range). */
10226
10279
  activePresetId = computed(() => this.presetIdFor(this.value()), ...(ngDevMode ? [{ debugName: "activePresetId" }] : []));
10227
10280
  pendingPresetId = computed(() => {
@@ -10290,9 +10343,12 @@ class SdsDateRangePicker {
10290
10343
  this.open.set(true);
10291
10344
  // Run after the overlay is rendered, then relocate it to <body>.
10292
10345
  afterNextRender(() => {
10293
- this.panelEl = this.host.nativeElement.querySelector('[role="dialog"]');
10294
- this.positionPanel(); // also relocates to document.body
10295
- this.addReflowListeners();
10346
+ const panel = this.host.nativeElement.querySelector('[role="dialog"]');
10347
+ const trigger = this.trigger()?.nativeElement;
10348
+ if (panel && trigger) {
10349
+ // gap 6, as this field has always used — the shared default is 4.
10350
+ this.overlay.attach(trigger, panel, { gap: 6 });
10351
+ }
10296
10352
  this.calendar()?.focusActiveCell();
10297
10353
  }, { injector: this.injector });
10298
10354
  }
@@ -10300,9 +10356,7 @@ class SdsDateRangePicker {
10300
10356
  close(returnFocus = false) {
10301
10357
  if (!this.open())
10302
10358
  return;
10303
- this.removeReflowListeners();
10304
- this.panelEl?.remove(); // pull the body-relocated node before destroying the view
10305
- this.panelEl = null;
10359
+ this.overlay.detach(); // pull the body-relocated node before destroying the view
10306
10360
  this.open.set(false);
10307
10361
  if (returnFocus)
10308
10362
  this.trigger()?.nativeElement.focus();
@@ -10339,17 +10393,18 @@ class SdsDateRangePicker {
10339
10393
  * modal's focus trap; this keeps Tab / Shift+Tab cycling within the panel's own focusables.
10340
10394
  */
10341
10395
  onPanelKeydown(event) {
10342
- if (event.key !== 'Tab' || !this.panelEl)
10396
+ const panel = this.overlay.element;
10397
+ if (event.key !== 'Tab' || !panel)
10343
10398
  return;
10344
10399
  const sel = 'a[href],button:not([disabled]):not([tabindex="-1"]),input:not([disabled]),' +
10345
10400
  '[tabindex]:not([tabindex="-1"])';
10346
- const items = Array.from(this.panelEl.querySelectorAll(sel)).filter((el) => el.offsetParent !== null);
10401
+ const items = Array.from(panel.querySelectorAll(sel)).filter((el) => el.offsetParent !== null);
10347
10402
  if (items.length === 0)
10348
10403
  return;
10349
10404
  const first = items[0];
10350
10405
  const last = items[items.length - 1];
10351
10406
  const active = document.activeElement;
10352
- if (event.shiftKey && (active === first || !this.panelEl.contains(active))) {
10407
+ if (event.shiftKey && (active === first || !panel.contains(active))) {
10353
10408
  event.preventDefault();
10354
10409
  last.focus();
10355
10410
  }
@@ -10364,58 +10419,13 @@ class SdsDateRangePicker {
10364
10419
  // composedPath is fixed at dispatch time, so a click on a node re-rendered away still counts.
10365
10420
  const path = event.composedPath();
10366
10421
  const inField = path.includes(this.host.nativeElement);
10367
- const inPanel = !!this.panelEl && path.includes(this.panelEl);
10422
+ const inPanel = !!this.overlay.element && path.includes(this.overlay.element);
10368
10423
  if (!inField && !inPanel)
10369
10424
  this.close();
10370
10425
  }
10371
10426
  // --- body-appended overlay positioning ---
10372
- reflow = () => this.positionPanel();
10373
- listening = false;
10374
- addReflowListeners() {
10375
- if (this.listening)
10376
- return;
10377
- window.addEventListener('scroll', this.reflow, true);
10378
- window.addEventListener('resize', this.reflow);
10379
- this.listening = true;
10380
- }
10381
- removeReflowListeners() {
10382
- if (!this.listening)
10383
- return;
10384
- window.removeEventListener('scroll', this.reflow, true);
10385
- window.removeEventListener('resize', this.reflow);
10386
- this.listening = false;
10387
- }
10388
- positionPanel() {
10389
- const triggerEl = this.trigger()?.nativeElement;
10390
- const panelEl = this.panelEl;
10391
- if (!triggerEl || !panelEl)
10392
- return;
10393
- if (panelEl.parentElement !== document.body) {
10394
- document.body.appendChild(panelEl);
10395
- }
10396
- const r = triggerEl.getBoundingClientRect();
10397
- const ph = panelEl.offsetHeight || 420;
10398
- const pw = panelEl.offsetWidth || 760;
10399
- const gap = 6;
10400
- const margin = 8;
10401
- // Below the field by default; flip above when there isn't room; else clamp into the viewport.
10402
- let top = r.bottom + gap;
10403
- if (top + ph > window.innerHeight - margin) {
10404
- if (r.top - gap - ph > margin)
10405
- top = r.top - gap - ph;
10406
- else
10407
- top = Math.max(margin, window.innerHeight - margin - ph);
10408
- }
10409
- let left = r.left;
10410
- if (left + pw > window.innerWidth - margin) {
10411
- left = Math.max(margin, window.innerWidth - margin - pw);
10412
- }
10413
- panelEl.style.top = `${Math.round(top)}px`;
10414
- panelEl.style.left = `${Math.round(left)}px`;
10415
- }
10416
10427
  ngOnDestroy() {
10417
- this.removeReflowListeners();
10418
- this.panelEl?.remove();
10428
+ this.overlay.detach();
10419
10429
  }
10420
10430
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsDateRangePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
10421
10431
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsDateRangePicker, isStandalone: true, selector: "sds-date-range-picker", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, presets: { classPropertyName: "presets", publicName: "presets", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, maxDays: { classPropertyName: "maxDays", publicName: "maxDays", isSignal: true, isRequired: false, transformFunction: null }, firstDayOfWeek: { classPropertyName: "firstDayOfWeek", publicName: "firstDayOfWeek", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, months: { classPropertyName: "months", publicName: "months", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, panelLabel: { classPropertyName: "panelLabel", publicName: "panelLabel", isSignal: true, isRequired: false, transformFunction: null }, presetsLabel: { classPropertyName: "presetsLabel", publicName: "presetsLabel", isSignal: true, isRequired: false, transformFunction: null }, startLabel: { classPropertyName: "startLabel", publicName: "startLabel", isSignal: true, isRequired: false, transformFunction: null }, endLabel: { classPropertyName: "endLabel", publicName: "endLabel", isSignal: true, isRequired: false, transformFunction: null }, applyLabel: { classPropertyName: "applyLabel", publicName: "applyLabel", isSignal: true, isRequired: false, transformFunction: null }, cancelLabel: { classPropertyName: "cancelLabel", publicName: "cancelLabel", isSignal: true, isRequired: false, transformFunction: null }, calendarLabel: { classPropertyName: "calendarLabel", publicName: "calendarLabel", isSignal: true, isRequired: false, transformFunction: null }, previousLabel: { classPropertyName: "previousLabel", publicName: "previousLabel", isSignal: true, isRequired: false, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", presetSelect: "presetSelect" }, host: { listeners: { "document:click": "onDocumentClick($event)", "keydown.escape": "onEscape($event)" }, classAttribute: "block" }, viewQueries: [{ propertyName: "calendar", first: true, predicate: SdsRangeCalendar, descendants: true, isSignal: true }, { propertyName: "trigger", first: true, predicate: ["trigger"], descendants: true, isSignal: true }], ngImport: i0, template: `
@@ -10433,7 +10443,9 @@ class SdsDateRangePicker {
10433
10443
  aria-haspopup="dialog"
10434
10444
  [attr.aria-expanded]="open()"
10435
10445
  [attr.aria-labelledby]="label() ? labelId + ' ' + valueId : null"
10436
- [attr.aria-label]="!label() && ariaLabel() ? ariaLabel() + ', ' + (displayValue() || placeholder()) : null"
10446
+ [attr.aria-label]="
10447
+ !label() && ariaLabel() ? ariaLabel() + ', ' + (displayValue() || placeholder()) : null
10448
+ "
10437
10449
  [class]="triggerClasses()"
10438
10450
  (click)="toggle($event)"
10439
10451
  (keydown.arrowdown)="onTriggerArrowDown($event)"
@@ -10458,7 +10470,7 @@ class SdsDateRangePicker {
10458
10470
 
10459
10471
  @if (open()) {
10460
10472
  <!-- Relocated to document.body on open (appendTo body) to escape ancestor overflow/stacking;
10461
- positioned via positionPanel(). -->
10473
+ placed by SdsOverlayPanel. -->
10462
10474
  <div
10463
10475
  class="fixed z-[1000] max-w-[calc(100vw-1rem)] overflow-auto rounded-card border border-border bg-surface shadow-lg"
10464
10476
  style="max-height: calc(100vh - 1rem)"
@@ -10475,7 +10487,9 @@ class SdsDateRangePicker {
10475
10487
  role="group"
10476
10488
  [attr.aria-label]="presetsLabel()"
10477
10489
  >
10478
- <p class="px-2 pb-1.5 pt-1 text-[11px] font-semibold uppercase tracking-wide text-ink-faint">
10490
+ <p
10491
+ class="px-2 pb-1.5 pt-1 text-[11px] font-semibold uppercase tracking-wide text-ink-faint"
10492
+ >
10479
10493
  {{ presetsLabel() }}
10480
10494
  </p>
10481
10495
  <div class="flex flex-wrap gap-1 sm:flex-col sm:flex-nowrap">
@@ -10515,15 +10529,27 @@ class SdsDateRangePicker {
10515
10529
  >
10516
10530
  <p class="flex items-stretch gap-2 text-sm" aria-live="polite">
10517
10531
  <span class="flex flex-col leading-tight">
10518
- <span class="text-[11px] font-medium uppercase text-ink-faint">{{ startLabel() }}</span>
10519
- <span class="font-medium" [class.text-ink]="pendingStart()" [class.text-ink-faint]="!pendingStart()">
10532
+ <span class="text-[11px] font-medium uppercase text-ink-faint">{{
10533
+ startLabel()
10534
+ }}</span>
10535
+ <span
10536
+ class="font-medium"
10537
+ [class.text-ink]="pendingStart()"
10538
+ [class.text-ink-faint]="!pendingStart()"
10539
+ >
10520
10540
  {{ pendingStartText() }}
10521
10541
  </span>
10522
10542
  </span>
10523
10543
  <span class="self-end text-ink-faint" aria-hidden="true">–</span>
10524
10544
  <span class="flex flex-col leading-tight">
10525
- <span class="text-[11px] font-medium uppercase text-ink-faint">{{ endLabel() }}</span>
10526
- <span class="font-medium" [class.text-ink]="pendingEnd()" [class.text-ink-faint]="!pendingEnd()">
10545
+ <span class="text-[11px] font-medium uppercase text-ink-faint">{{
10546
+ endLabel()
10547
+ }}</span>
10548
+ <span
10549
+ class="font-medium"
10550
+ [class.text-ink]="pendingEnd()"
10551
+ [class.text-ink-faint]="!pendingEnd()"
10552
+ >
10527
10553
  {{ pendingEndText() }}
10528
10554
  </span>
10529
10555
  </span>
@@ -10569,7 +10595,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
10569
10595
  aria-haspopup="dialog"
10570
10596
  [attr.aria-expanded]="open()"
10571
10597
  [attr.aria-labelledby]="label() ? labelId + ' ' + valueId : null"
10572
- [attr.aria-label]="!label() && ariaLabel() ? ariaLabel() + ', ' + (displayValue() || placeholder()) : null"
10598
+ [attr.aria-label]="
10599
+ !label() && ariaLabel() ? ariaLabel() + ', ' + (displayValue() || placeholder()) : null
10600
+ "
10573
10601
  [class]="triggerClasses()"
10574
10602
  (click)="toggle($event)"
10575
10603
  (keydown.arrowdown)="onTriggerArrowDown($event)"
@@ -10594,7 +10622,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
10594
10622
 
10595
10623
  @if (open()) {
10596
10624
  <!-- Relocated to document.body on open (appendTo body) to escape ancestor overflow/stacking;
10597
- positioned via positionPanel(). -->
10625
+ placed by SdsOverlayPanel. -->
10598
10626
  <div
10599
10627
  class="fixed z-[1000] max-w-[calc(100vw-1rem)] overflow-auto rounded-card border border-border bg-surface shadow-lg"
10600
10628
  style="max-height: calc(100vh - 1rem)"
@@ -10611,7 +10639,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
10611
10639
  role="group"
10612
10640
  [attr.aria-label]="presetsLabel()"
10613
10641
  >
10614
- <p class="px-2 pb-1.5 pt-1 text-[11px] font-semibold uppercase tracking-wide text-ink-faint">
10642
+ <p
10643
+ class="px-2 pb-1.5 pt-1 text-[11px] font-semibold uppercase tracking-wide text-ink-faint"
10644
+ >
10615
10645
  {{ presetsLabel() }}
10616
10646
  </p>
10617
10647
  <div class="flex flex-wrap gap-1 sm:flex-col sm:flex-nowrap">
@@ -10651,15 +10681,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
10651
10681
  >
10652
10682
  <p class="flex items-stretch gap-2 text-sm" aria-live="polite">
10653
10683
  <span class="flex flex-col leading-tight">
10654
- <span class="text-[11px] font-medium uppercase text-ink-faint">{{ startLabel() }}</span>
10655
- <span class="font-medium" [class.text-ink]="pendingStart()" [class.text-ink-faint]="!pendingStart()">
10684
+ <span class="text-[11px] font-medium uppercase text-ink-faint">{{
10685
+ startLabel()
10686
+ }}</span>
10687
+ <span
10688
+ class="font-medium"
10689
+ [class.text-ink]="pendingStart()"
10690
+ [class.text-ink-faint]="!pendingStart()"
10691
+ >
10656
10692
  {{ pendingStartText() }}
10657
10693
  </span>
10658
10694
  </span>
10659
10695
  <span class="self-end text-ink-faint" aria-hidden="true">–</span>
10660
10696
  <span class="flex flex-col leading-tight">
10661
- <span class="text-[11px] font-medium uppercase text-ink-faint">{{ endLabel() }}</span>
10662
- <span class="font-medium" [class.text-ink]="pendingEnd()" [class.text-ink-faint]="!pendingEnd()">
10697
+ <span class="text-[11px] font-medium uppercase text-ink-faint">{{
10698
+ endLabel()
10699
+ }}</span>
10700
+ <span
10701
+ class="font-medium"
10702
+ [class.text-ink]="pendingEnd()"
10703
+ [class.text-ink-faint]="!pendingEnd()"
10704
+ >
10663
10705
  {{ pendingEndText() }}
10664
10706
  </span>
10665
10707
  </span>
@@ -10838,6 +10880,8 @@ class SdsSelect {
10838
10880
  cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : []));
10839
10881
  trigger = viewChild('trigger', ...(ngDevMode ? [{ debugName: "trigger" }] : []));
10840
10882
  panelEl = null;
10883
+ /** Body-appending, positioning, follow-on-scroll and teardown — see SdsOverlayPanel. */
10884
+ overlay = new SdsOverlayPanel();
10841
10885
  isDisabled = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
10842
10886
  filled = computed(() => this.appearance() === 'filled', ...(ngDevMode ? [{ debugName: "filled" }] : []));
10843
10887
  onBrand = computed(() => this.appearance() === 'on-brand', ...(ngDevMode ? [{ debugName: "onBrand" }] : []));
@@ -10882,9 +10926,7 @@ class SdsSelect {
10882
10926
  ? 'border-danger focus:border-danger focus:ring-2 focus:ring-danger/25'
10883
10927
  : 'border-border focus:border-brand-500 focus:ring-2 focus:ring-brand-500/25');
10884
10928
  }
10885
- parts.push(this.isDisabled()
10886
- ? 'opacity-60 cursor-not-allowed pointer-events-none'
10887
- : 'cursor-pointer');
10929
+ parts.push(this.isDisabled() ? 'opacity-60 cursor-not-allowed pointer-events-none' : 'cursor-pointer');
10888
10930
  return parts.join(' ');
10889
10931
  }, ...(ngDevMode ? [{ debugName: "wrapperClasses" }] : []));
10890
10932
  // --- option normalization & equality ---
@@ -10918,7 +10960,11 @@ class SdsSelect {
10918
10960
  }, ...(ngDevMode ? [{ debugName: "triggerContext" }] : []));
10919
10961
  /** Context for a projected footer template. */
10920
10962
  footerContext() {
10921
- return { $implicit: this.filterText(), filterText: this.filterText(), close: () => this.close(true) };
10963
+ return {
10964
+ $implicit: this.filterText(),
10965
+ filterText: this.filterText(),
10966
+ close: () => this.close(true),
10967
+ };
10922
10968
  }
10923
10969
  equals(a, b) {
10924
10970
  const k = this.dataKey();
@@ -10958,9 +11004,9 @@ class SdsSelect {
10958
11004
  this.onTouched();
10959
11005
  this.initHighlight();
10960
11006
  afterNextRender(() => {
10961
- this.panelEl = this.host.nativeElement.querySelector('[data-sds-select-panel]');
10962
- this.positionPanel();
10963
- this.addReflowListeners();
11007
+ this.panelEl =
11008
+ this.host.nativeElement.querySelector('[data-sds-select-panel]');
11009
+ this.attachPanel();
10964
11010
  if (this.filter()) {
10965
11011
  this.panelEl?.querySelector('input')?.focus();
10966
11012
  }
@@ -10973,8 +11019,7 @@ class SdsSelect {
10973
11019
  close(returnFocus = false) {
10974
11020
  if (!this.open())
10975
11021
  return;
10976
- this.removeReflowListeners();
10977
- this.panelEl?.remove();
11022
+ this.overlay.detach();
10978
11023
  this.panelEl = null;
10979
11024
  this.open.set(false);
10980
11025
  if (returnFocus)
@@ -10991,7 +11036,7 @@ class SdsSelect {
10991
11036
  onFilter(event) {
10992
11037
  this.filterText.set(event.target.value);
10993
11038
  this.highlightFirst();
10994
- setTimeout(() => this.positionPanel());
11039
+ setTimeout(() => this.overlay.reposition());
10995
11040
  }
10996
11041
  // --- keyboard ---
10997
11042
  onTriggerKeydown(event) {
@@ -11047,7 +11092,8 @@ class SdsSelect {
11047
11092
  case 'Tab':
11048
11093
  this.close(false);
11049
11094
  return true;
11050
- default: return false;
11095
+ default:
11096
+ return false;
11051
11097
  }
11052
11098
  }
11053
11099
  initHighlight() {
@@ -11110,46 +11156,12 @@ class SdsSelect {
11110
11156
  el?.scrollIntoView({ block: 'nearest' });
11111
11157
  });
11112
11158
  }
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;
11145
- }
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);
11159
+ /** Attach once the panel exists — it is rendered inside an `@if`. */
11160
+ attachPanel() {
11161
+ const trigger = this.trigger()?.nativeElement;
11162
+ if (trigger && this.panelEl) {
11163
+ this.overlay.attach(trigger, this.panelEl, { width: 'min' });
11150
11164
  }
11151
- panelEl.style.top = `${Math.round(top)}px`;
11152
- panelEl.style.left = `${Math.round(left)}px`;
11153
11165
  }
11154
11166
  onDocumentClick(event) {
11155
11167
  if (!this.open())
@@ -11161,9 +11173,8 @@ class SdsSelect {
11161
11173
  this.close();
11162
11174
  }
11163
11175
  ngOnDestroy() {
11164
- this.removeReflowListeners();
11165
11176
  clearTimeout(this.typeTimer);
11166
- this.panelEl?.remove();
11177
+ this.overlay.detach();
11167
11178
  }
11168
11179
  setValue(v) {
11169
11180
  this.value.set(v);
@@ -11224,10 +11235,7 @@ class SdsSelect {
11224
11235
  />
11225
11236
  </span>
11226
11237
  } @else {
11227
- <span
11228
- class="flex-1 truncate pl-3.5"
11229
- [class]="valueTextClass()"
11230
- >
11238
+ <span class="flex-1 truncate pl-3.5" [class]="valueTextClass()">
11231
11239
  {{ selectedOption() ? selectedOption()!.label : placeholder() }}
11232
11240
  </span>
11233
11241
  }
@@ -11238,7 +11246,11 @@ class SdsSelect {
11238
11246
  tabindex="-1"
11239
11247
  [attr.aria-label]="clearLabel()"
11240
11248
  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'"
11249
+ [class]="
11250
+ onBrand()
11251
+ ? 'text-white/60 hover:bg-white/10 hover:text-white'
11252
+ : 'text-ink-faint hover:bg-black/5 hover:text-ink-soft'
11253
+ "
11242
11254
  (click)="clear($event)"
11243
11255
  >
11244
11256
  <sds-icon name="x" [size]="15" />
@@ -11257,11 +11269,20 @@ class SdsSelect {
11257
11269
 
11258
11270
  @if (open()) {
11259
11271
  <!-- 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">
11272
+ <div
11273
+ data-sds-select-panel
11274
+ class="fixed z-[1000] overflow-hidden rounded-card border border-border bg-surface shadow-lg"
11275
+ >
11261
11276
  @if (filter()) {
11262
11277
  <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" />
11278
+ <div
11279
+ class="relative flex items-center rounded-control border border-border focus-within:border-brand-500"
11280
+ >
11281
+ <sds-icon
11282
+ name="search"
11283
+ [size]="16"
11284
+ class="pointer-events-none absolute left-2.5 text-ink-faint"
11285
+ />
11265
11286
  <input
11266
11287
  type="text"
11267
11288
  role="combobox"
@@ -11310,7 +11331,10 @@ class SdsSelect {
11310
11331
  <li class="px-3 py-2 text-sm text-ink-faint">
11311
11332
  <ng-container
11312
11333
  [ngTemplateOutlet]="emptyTpl()!.template"
11313
- [ngTemplateOutletContext]="{ $implicit: filterText(), filterText: filterText() }"
11334
+ [ngTemplateOutletContext]="{
11335
+ $implicit: filterText(),
11336
+ filterText: filterText(),
11337
+ }"
11314
11338
  />
11315
11339
  </li>
11316
11340
  } @else {
@@ -11386,10 +11410,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
11386
11410
  />
11387
11411
  </span>
11388
11412
  } @else {
11389
- <span
11390
- class="flex-1 truncate pl-3.5"
11391
- [class]="valueTextClass()"
11392
- >
11413
+ <span class="flex-1 truncate pl-3.5" [class]="valueTextClass()">
11393
11414
  {{ selectedOption() ? selectedOption()!.label : placeholder() }}
11394
11415
  </span>
11395
11416
  }
@@ -11400,7 +11421,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
11400
11421
  tabindex="-1"
11401
11422
  [attr.aria-label]="clearLabel()"
11402
11423
  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'"
11424
+ [class]="
11425
+ onBrand()
11426
+ ? 'text-white/60 hover:bg-white/10 hover:text-white'
11427
+ : 'text-ink-faint hover:bg-black/5 hover:text-ink-soft'
11428
+ "
11404
11429
  (click)="clear($event)"
11405
11430
  >
11406
11431
  <sds-icon name="x" [size]="15" />
@@ -11419,11 +11444,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
11419
11444
 
11420
11445
  @if (open()) {
11421
11446
  <!-- 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">
11447
+ <div
11448
+ data-sds-select-panel
11449
+ class="fixed z-[1000] overflow-hidden rounded-card border border-border bg-surface shadow-lg"
11450
+ >
11423
11451
  @if (filter()) {
11424
11452
  <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" />
11453
+ <div
11454
+ class="relative flex items-center rounded-control border border-border focus-within:border-brand-500"
11455
+ >
11456
+ <sds-icon
11457
+ name="search"
11458
+ [size]="16"
11459
+ class="pointer-events-none absolute left-2.5 text-ink-faint"
11460
+ />
11427
11461
  <input
11428
11462
  type="text"
11429
11463
  role="combobox"
@@ -11472,7 +11506,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
11472
11506
  <li class="px-3 py-2 text-sm text-ink-faint">
11473
11507
  <ng-container
11474
11508
  [ngTemplateOutlet]="emptyTpl()!.template"
11475
- [ngTemplateOutletContext]="{ $implicit: filterText(), filterText: filterText() }"
11509
+ [ngTemplateOutletContext]="{
11510
+ $implicit: filterText(),
11511
+ filterText: filterText(),
11512
+ }"
11476
11513
  />
11477
11514
  </li>
11478
11515
  } @else {
@@ -11596,6 +11633,8 @@ class SdsMultiSelect {
11596
11633
  cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : []));
11597
11634
  trigger = viewChild('trigger', ...(ngDevMode ? [{ debugName: "trigger" }] : []));
11598
11635
  panelEl = null;
11636
+ /** Body-appending, positioning, follow-on-scroll and teardown — see SdsOverlayPanel. */
11637
+ overlay = new SdsOverlayPanel();
11599
11638
  isDisabled = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
11600
11639
  normalizedOptions = computed(() => this.options().map((o) => this.norm(o)), ...(ngDevMode ? [{ debugName: "normalizedOptions" }] : []));
11601
11640
  filteredOptions = computed(() => {
@@ -11641,9 +11680,7 @@ class SdsMultiSelect {
11641
11680
  parts.push(this.error()
11642
11681
  ? 'border-danger focus:border-danger focus:ring-2 focus:ring-danger/25'
11643
11682
  : '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');
11683
+ parts.push(this.isDisabled() ? 'opacity-60 cursor-not-allowed pointer-events-none' : 'cursor-pointer');
11647
11684
  return parts.join(' ');
11648
11685
  }, ...(ngDevMode ? [{ debugName: "wrapperClasses" }] : []));
11649
11686
  // --- normalization & equality ---
@@ -11672,7 +11709,11 @@ class SdsMultiSelect {
11672
11709
  }
11673
11710
  /** Context for a projected footer template. */
11674
11711
  footerContext() {
11675
- return { $implicit: this.filterText(), filterText: this.filterText(), close: () => this.close(true) };
11712
+ return {
11713
+ $implicit: this.filterText(),
11714
+ filterText: this.filterText(),
11715
+ close: () => this.close(true),
11716
+ };
11676
11717
  }
11677
11718
  equals(a, b) {
11678
11719
  const k = this.dataKey();
@@ -11718,9 +11759,9 @@ class SdsMultiSelect {
11718
11759
  this.onTouched();
11719
11760
  this.initHighlight();
11720
11761
  afterNextRender(() => {
11721
- this.panelEl = this.host.nativeElement.querySelector('[data-sds-select-panel]');
11722
- this.positionPanel();
11723
- this.addReflowListeners();
11762
+ this.panelEl =
11763
+ this.host.nativeElement.querySelector('[data-sds-select-panel]');
11764
+ this.attachPanel();
11724
11765
  if (this.filter()) {
11725
11766
  this.panelEl?.querySelector('input')?.focus();
11726
11767
  }
@@ -11733,8 +11774,7 @@ class SdsMultiSelect {
11733
11774
  close(returnFocus = false) {
11734
11775
  if (!this.open())
11735
11776
  return;
11736
- this.removeReflowListeners();
11737
- this.panelEl?.remove();
11777
+ this.overlay.detach();
11738
11778
  this.panelEl = null;
11739
11779
  this.open.set(false);
11740
11780
  if (returnFocus)
@@ -11752,9 +11792,7 @@ class SdsMultiSelect {
11752
11792
  toggleOption(opt) {
11753
11793
  const cur = this.value();
11754
11794
  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];
11795
+ const next = exists ? cur.filter((x) => !this.equals(x, opt.value)) : [...cur, opt.value];
11758
11796
  this.setValue(next);
11759
11797
  }
11760
11798
  toggleAll() {
@@ -11770,7 +11808,7 @@ class SdsMultiSelect {
11770
11808
  onFilter(event) {
11771
11809
  this.filterText.set(event.target.value);
11772
11810
  this.highlightFirst();
11773
- setTimeout(() => this.positionPanel());
11811
+ setTimeout(() => this.overlay.reposition());
11774
11812
  }
11775
11813
  // --- keyboard ---
11776
11814
  onTriggerKeydown(event) {
@@ -11835,7 +11873,8 @@ class SdsMultiSelect {
11835
11873
  case 'Tab':
11836
11874
  this.close(false);
11837
11875
  return true;
11838
- default: return false;
11876
+ default:
11877
+ return false;
11839
11878
  }
11840
11879
  }
11841
11880
  initHighlight() {
@@ -11903,46 +11942,12 @@ class SdsMultiSelect {
11903
11942
  el?.scrollIntoView({ block: 'nearest' });
11904
11943
  });
11905
11944
  }
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;
11945
+ /** Attach once the panel exists — it is rendered inside an `@if`. */
11946
+ attachPanel() {
11947
+ const trigger = this.trigger()?.nativeElement;
11948
+ if (trigger && this.panelEl) {
11949
+ this.overlay.attach(trigger, this.panelEl, { width: 'min' });
11938
11950
  }
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);
11943
- }
11944
- panelEl.style.top = `${Math.round(top)}px`;
11945
- panelEl.style.left = `${Math.round(left)}px`;
11946
11951
  }
11947
11952
  onDocumentClick(event) {
11948
11953
  if (!this.open())
@@ -11954,9 +11959,8 @@ class SdsMultiSelect {
11954
11959
  this.close();
11955
11960
  }
11956
11961
  ngOnDestroy() {
11957
- this.removeReflowListeners();
11958
11962
  clearTimeout(this.typeTimer);
11959
- this.panelEl?.remove();
11963
+ this.overlay.detach();
11960
11964
  }
11961
11965
  setValue(v) {
11962
11966
  this.value.set(v);
@@ -11964,7 +11968,7 @@ class SdsMultiSelect {
11964
11968
  this.onTouched();
11965
11969
  // Chip display can change the trigger height while the panel is open.
11966
11970
  if (this.open())
11967
- setTimeout(() => this.positionPanel());
11971
+ setTimeout(() => this.overlay.reposition());
11968
11972
  }
11969
11973
  // --- ControlValueAccessor ---
11970
11974
  onChange = () => { };
@@ -12012,7 +12016,9 @@ class SdsMultiSelect {
12012
12016
  @if (display() === 'chip' && selectedOptions().length) {
12013
12017
  <div class="flex flex-1 flex-wrap items-center gap-1 py-1 pl-2">
12014
12018
  @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">
12019
+ <span
12020
+ 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"
12021
+ >
12016
12022
  {{ opt.label }}
12017
12023
  <button
12018
12024
  type="button"
@@ -12057,11 +12063,20 @@ class SdsMultiSelect {
12057
12063
 
12058
12064
  @if (open()) {
12059
12065
  <!-- 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">
12066
+ <div
12067
+ data-sds-select-panel
12068
+ class="fixed z-[1000] overflow-hidden rounded-card border border-border bg-surface shadow-lg"
12069
+ >
12061
12070
  @if (filter()) {
12062
12071
  <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" />
12072
+ <div
12073
+ class="relative flex items-center rounded-control border border-border focus-within:border-brand-500"
12074
+ >
12075
+ <sds-icon
12076
+ name="search"
12077
+ [size]="16"
12078
+ class="pointer-events-none absolute left-2.5 text-ink-faint"
12079
+ />
12065
12080
  <input
12066
12081
  type="text"
12067
12082
  role="combobox"
@@ -12085,13 +12100,18 @@ class SdsMultiSelect {
12085
12100
  role="checkbox"
12086
12101
  [id]="toggleAllId"
12087
12102
  [attr.data-index]="-1"
12088
- [attr.aria-checked]="allFilteredSelected() ? 'true' : (toggleAllIndeterminate() ? 'mixed' : 'false')"
12103
+ [attr.aria-checked]="
12104
+ allFilteredSelected() ? 'true' : toggleAllIndeterminate() ? 'mixed' : 'false'
12105
+ "
12089
12106
  class="flex w-full items-center gap-2.5 border-b border-border px-3 py-2 text-sm text-ink"
12090
12107
  [class.bg-brand-50]="highlightedIndex() === -1"
12091
12108
  (click)="toggleAll()"
12092
12109
  (mouseenter)="highlightedIndex.set(-1)"
12093
12110
  >
12094
- <span class="grid h-[18px] w-[18px] shrink-0 place-items-center rounded border" [class]="checkboxClass(allFilteredSelected(), toggleAllIndeterminate())">
12111
+ <span
12112
+ class="grid h-[18px] w-[18px] shrink-0 place-items-center rounded border"
12113
+ [class]="checkboxClass(allFilteredSelected(), toggleAllIndeterminate())"
12114
+ >
12095
12115
  @if (allFilteredSelected()) {
12096
12116
  <sds-icon name="check" [size]="12" />
12097
12117
  } @else if (toggleAllIndeterminate()) {
@@ -12102,7 +12122,12 @@ class SdsMultiSelect {
12102
12122
  </button>
12103
12123
  }
12104
12124
 
12105
- <ul [id]="listId" role="listbox" aria-multiselectable="true" class="max-h-60 overflow-auto py-1">
12125
+ <ul
12126
+ [id]="listId"
12127
+ role="listbox"
12128
+ aria-multiselectable="true"
12129
+ class="max-h-60 overflow-auto py-1"
12130
+ >
12106
12131
  @for (opt of filteredOptions(); track $index; let i = $index) {
12107
12132
  <li
12108
12133
  [id]="optionId(i)"
@@ -12114,7 +12139,10 @@ class SdsMultiSelect {
12114
12139
  (click)="opt.disabled ? null : toggleOption(opt)"
12115
12140
  (mouseenter)="opt.disabled ? null : highlightedIndex.set(i)"
12116
12141
  >
12117
- <span class="grid h-[18px] w-[18px] shrink-0 place-items-center rounded border" [class]="checkboxClass(isSelected(opt.value), false)">
12142
+ <span
12143
+ class="grid h-[18px] w-[18px] shrink-0 place-items-center rounded border"
12144
+ [class]="checkboxClass(isSelected(opt.value), false)"
12145
+ >
12118
12146
  @if (isSelected(opt.value)) {
12119
12147
  <sds-icon name="check" [size]="12" />
12120
12148
  }
@@ -12135,7 +12163,10 @@ class SdsMultiSelect {
12135
12163
  <li class="px-3 py-2 text-sm text-ink-faint">
12136
12164
  <ng-container
12137
12165
  [ngTemplateOutlet]="emptyTpl()!.template"
12138
- [ngTemplateOutletContext]="{ $implicit: filterText(), filterText: filterText() }"
12166
+ [ngTemplateOutletContext]="{
12167
+ $implicit: filterText(),
12168
+ filterText: filterText(),
12169
+ }"
12139
12170
  />
12140
12171
  </li>
12141
12172
  } @else {
@@ -12203,7 +12234,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
12203
12234
  @if (display() === 'chip' && selectedOptions().length) {
12204
12235
  <div class="flex flex-1 flex-wrap items-center gap-1 py-1 pl-2">
12205
12236
  @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">
12237
+ <span
12238
+ 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"
12239
+ >
12207
12240
  {{ opt.label }}
12208
12241
  <button
12209
12242
  type="button"
@@ -12248,11 +12281,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
12248
12281
 
12249
12282
  @if (open()) {
12250
12283
  <!-- 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">
12284
+ <div
12285
+ data-sds-select-panel
12286
+ class="fixed z-[1000] overflow-hidden rounded-card border border-border bg-surface shadow-lg"
12287
+ >
12252
12288
  @if (filter()) {
12253
12289
  <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" />
12290
+ <div
12291
+ class="relative flex items-center rounded-control border border-border focus-within:border-brand-500"
12292
+ >
12293
+ <sds-icon
12294
+ name="search"
12295
+ [size]="16"
12296
+ class="pointer-events-none absolute left-2.5 text-ink-faint"
12297
+ />
12256
12298
  <input
12257
12299
  type="text"
12258
12300
  role="combobox"
@@ -12276,13 +12318,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
12276
12318
  role="checkbox"
12277
12319
  [id]="toggleAllId"
12278
12320
  [attr.data-index]="-1"
12279
- [attr.aria-checked]="allFilteredSelected() ? 'true' : (toggleAllIndeterminate() ? 'mixed' : 'false')"
12321
+ [attr.aria-checked]="
12322
+ allFilteredSelected() ? 'true' : toggleAllIndeterminate() ? 'mixed' : 'false'
12323
+ "
12280
12324
  class="flex w-full items-center gap-2.5 border-b border-border px-3 py-2 text-sm text-ink"
12281
12325
  [class.bg-brand-50]="highlightedIndex() === -1"
12282
12326
  (click)="toggleAll()"
12283
12327
  (mouseenter)="highlightedIndex.set(-1)"
12284
12328
  >
12285
- <span class="grid h-[18px] w-[18px] shrink-0 place-items-center rounded border" [class]="checkboxClass(allFilteredSelected(), toggleAllIndeterminate())">
12329
+ <span
12330
+ class="grid h-[18px] w-[18px] shrink-0 place-items-center rounded border"
12331
+ [class]="checkboxClass(allFilteredSelected(), toggleAllIndeterminate())"
12332
+ >
12286
12333
  @if (allFilteredSelected()) {
12287
12334
  <sds-icon name="check" [size]="12" />
12288
12335
  } @else if (toggleAllIndeterminate()) {
@@ -12293,7 +12340,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
12293
12340
  </button>
12294
12341
  }
12295
12342
 
12296
- <ul [id]="listId" role="listbox" aria-multiselectable="true" class="max-h-60 overflow-auto py-1">
12343
+ <ul
12344
+ [id]="listId"
12345
+ role="listbox"
12346
+ aria-multiselectable="true"
12347
+ class="max-h-60 overflow-auto py-1"
12348
+ >
12297
12349
  @for (opt of filteredOptions(); track $index; let i = $index) {
12298
12350
  <li
12299
12351
  [id]="optionId(i)"
@@ -12305,7 +12357,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
12305
12357
  (click)="opt.disabled ? null : toggleOption(opt)"
12306
12358
  (mouseenter)="opt.disabled ? null : highlightedIndex.set(i)"
12307
12359
  >
12308
- <span class="grid h-[18px] w-[18px] shrink-0 place-items-center rounded border" [class]="checkboxClass(isSelected(opt.value), false)">
12360
+ <span
12361
+ class="grid h-[18px] w-[18px] shrink-0 place-items-center rounded border"
12362
+ [class]="checkboxClass(isSelected(opt.value), false)"
12363
+ >
12309
12364
  @if (isSelected(opt.value)) {
12310
12365
  <sds-icon name="check" [size]="12" />
12311
12366
  }
@@ -12326,7 +12381,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
12326
12381
  <li class="px-3 py-2 text-sm text-ink-faint">
12327
12382
  <ng-container
12328
12383
  [ngTemplateOutlet]="emptyTpl()!.template"
12329
- [ngTemplateOutletContext]="{ $implicit: filterText(), filterText: filterText() }"
12384
+ [ngTemplateOutletContext]="{
12385
+ $implicit: filterText(),
12386
+ filterText: filterText(),
12387
+ }"
12330
12388
  />
12331
12389
  </li>
12332
12390
  } @else {
@@ -12450,15 +12508,8 @@ class SdsAutocomplete {
12450
12508
  cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : []));
12451
12509
  /** Every option this control has seen, by value — how a chip keeps its label after a new search. */
12452
12510
  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;
12511
+ /** Body-appending, positioning, follow-on-scroll and teardown all live in here now. */
12512
+ overlay = new SdsOverlayPanel();
12462
12513
  timer;
12463
12514
  seq = 0;
12464
12515
  destroyed = false;
@@ -12490,7 +12541,7 @@ class SdsAutocomplete {
12490
12541
  }, ...(ngDevMode ? [{ debugName: "triggerPlaceholder" }] : []));
12491
12542
  closeFn = () => this.close();
12492
12543
  constructor() {
12493
- afterNextRender(() => this.position(), { injector: this.injector });
12544
+ afterNextRender(() => this.place(), { injector: this.injector });
12494
12545
  }
12495
12546
  // ------------------------------------------------------------------ options
12496
12547
  labelOf(option) {
@@ -12553,8 +12604,7 @@ class SdsAutocomplete {
12553
12604
  this.open.set(true);
12554
12605
  this.opened.emit();
12555
12606
  this.schedule(0);
12556
- setTimeout(() => this.position());
12557
- this.addReflow();
12607
+ setTimeout(() => this.place());
12558
12608
  }
12559
12609
  toggle(event) {
12560
12610
  event.stopPropagation();
@@ -12598,7 +12648,8 @@ class SdsAutocomplete {
12598
12648
  finally {
12599
12649
  if (seq === this.seq && !this.destroyed) {
12600
12650
  this.loading.set(false);
12601
- setTimeout(() => this.position());
12651
+ // The panel's height changed with its contents, so it may now need flipping.
12652
+ setTimeout(() => this.overlay.reposition());
12602
12653
  }
12603
12654
  }
12604
12655
  }
@@ -12682,67 +12733,29 @@ class SdsAutocomplete {
12682
12733
  return;
12683
12734
  this.open.set(false);
12684
12735
  this.active.set(-1);
12685
- this.removeReflow();
12686
- this.panelEl?.remove();
12687
- this.panelEl = null;
12736
+ this.overlay.detach();
12688
12737
  this.closed.emit(this.value());
12689
12738
  }
12690
12739
  onDocumentClick(event) {
12691
12740
  if (!this.open())
12692
12741
  return;
12693
12742
  const target = event.target;
12694
- if (!this.host.nativeElement.contains(target) && !this.panelEl?.contains(target)) {
12743
+ if (!this.host.nativeElement.contains(target) && !this.overlay.contains(target)) {
12695
12744
  this.close();
12696
12745
  }
12697
12746
  }
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() {
12747
+ /** Attach once the panel exists in the DOM — it is inside an `@if`. */
12748
+ place() {
12716
12749
  const trigger = this.triggerEl()?.nativeElement;
12717
12750
  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;
12751
+ if (trigger && panel) {
12752
+ this.overlay.attach(trigger, panel, { width: 'min' });
12731
12753
  }
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
12754
  }
12740
12755
  ngOnDestroy() {
12741
12756
  this.destroyed = true;
12742
12757
  clearTimeout(this.timer);
12743
- this.removeReflow();
12744
- this.panelEl?.remove();
12745
- this.panelEl = null;
12758
+ this.overlay.detach();
12746
12759
  }
12747
12760
  setValue(v) {
12748
12761
  this.value.set(v);
@@ -13110,26 +13123,166 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
13110
13123
  }], 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
13124
 
13112
13125
  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' },
13126
+ {
13127
+ iso2: 'BE',
13128
+ name: 'Belgium',
13129
+ dialCode: '32',
13130
+ lengths: [9],
13131
+ format: '### ## ## ##',
13132
+ example: '470123456',
13133
+ },
13134
+ {
13135
+ iso2: 'FR',
13136
+ name: 'France',
13137
+ dialCode: '33',
13138
+ lengths: [9],
13139
+ format: '# ## ## ## ##',
13140
+ example: '612345678',
13141
+ },
13142
+ {
13143
+ iso2: 'LU',
13144
+ name: 'Luxembourg',
13145
+ dialCode: '352',
13146
+ lengths: [9, 8, 6],
13147
+ format: '### ### ###',
13148
+ example: '628123456',
13149
+ },
13150
+ {
13151
+ iso2: 'NL',
13152
+ name: 'Netherlands',
13153
+ dialCode: '31',
13154
+ lengths: [9],
13155
+ format: '# ## ## ## ##',
13156
+ example: '612345678',
13157
+ },
13158
+ {
13159
+ iso2: 'DE',
13160
+ name: 'Germany',
13161
+ dialCode: '49',
13162
+ lengths: [10, 11],
13163
+ format: '#### #######',
13164
+ example: '15123456789',
13165
+ },
13166
+ {
13167
+ iso2: 'GB',
13168
+ name: 'United Kingdom',
13169
+ dialCode: '44',
13170
+ lengths: [10],
13171
+ format: '#### ######',
13172
+ example: '7400123456',
13173
+ },
13174
+ {
13175
+ iso2: 'IE',
13176
+ name: 'Ireland',
13177
+ dialCode: '353',
13178
+ lengths: [9],
13179
+ format: '## ### ####',
13180
+ example: '850123456',
13181
+ },
13182
+ {
13183
+ iso2: 'CH',
13184
+ name: 'Switzerland',
13185
+ dialCode: '41',
13186
+ lengths: [9],
13187
+ format: '## ### ## ##',
13188
+ example: '781234567',
13189
+ },
13190
+ {
13191
+ iso2: 'ES',
13192
+ name: 'Spain',
13193
+ dialCode: '34',
13194
+ lengths: [9],
13195
+ format: '### ## ## ##',
13196
+ example: '612345678',
13197
+ },
13198
+ {
13199
+ iso2: 'IT',
13200
+ name: 'Italy',
13201
+ dialCode: '39',
13202
+ lengths: [9, 10],
13203
+ format: '### ### ####',
13204
+ example: '3123456789',
13205
+ },
13206
+ {
13207
+ iso2: 'PT',
13208
+ name: 'Portugal',
13209
+ dialCode: '351',
13210
+ lengths: [9],
13211
+ format: '### ### ###',
13212
+ example: '912345678',
13213
+ },
13214
+ {
13215
+ iso2: 'AT',
13216
+ name: 'Austria',
13217
+ dialCode: '43',
13218
+ lengths: [10, 11],
13219
+ format: '### #######',
13220
+ example: '6641234567',
13221
+ },
13222
+ {
13223
+ iso2: 'DK',
13224
+ name: 'Denmark',
13225
+ dialCode: '45',
13226
+ lengths: [8],
13227
+ format: '## ## ## ##',
13228
+ example: '20123456',
13229
+ },
13230
+ {
13231
+ iso2: 'SE',
13232
+ name: 'Sweden',
13233
+ dialCode: '46',
13234
+ lengths: [9],
13235
+ format: '## ### ## ##',
13236
+ example: '701234567',
13237
+ },
13238
+ {
13239
+ iso2: 'NO',
13240
+ name: 'Norway',
13241
+ dialCode: '47',
13242
+ lengths: [8],
13243
+ format: '### ## ###',
13244
+ example: '40612345',
13245
+ },
13246
+ {
13247
+ iso2: 'PL',
13248
+ name: 'Poland',
13249
+ dialCode: '48',
13250
+ lengths: [9],
13251
+ format: '### ### ###',
13252
+ example: '512345678',
13253
+ },
13254
+ {
13255
+ iso2: 'US',
13256
+ name: 'United States',
13257
+ dialCode: '1',
13258
+ lengths: [10],
13259
+ format: '(###) ###-####',
13260
+ example: '2015550123',
13261
+ },
13262
+ {
13263
+ iso2: 'CA',
13264
+ name: 'Canada',
13265
+ dialCode: '1',
13266
+ lengths: [10],
13267
+ format: '(###) ###-####',
13268
+ example: '2042345678',
13269
+ },
13270
+ {
13271
+ iso2: 'MA',
13272
+ name: 'Morocco',
13273
+ dialCode: '212',
13274
+ lengths: [9],
13275
+ format: '### ## ## ##',
13276
+ example: '612345678',
13277
+ },
13278
+ {
13279
+ iso2: 'TR',
13280
+ name: 'Turkey',
13281
+ dialCode: '90',
13282
+ lengths: [10],
13283
+ format: '### ### ## ##',
13284
+ example: '5012345678',
13285
+ },
13133
13286
  ];
13134
13287
  /** Reduce raw input to national-significant digits (drops non-digits & leading trunk zeros). */
13135
13288
  function sdsNormalizeSignificant(raw) {
@@ -13280,6 +13433,8 @@ class SdsPhoneInput {
13280
13433
  anchor = viewChild('anchor', ...(ngDevMode ? [{ debugName: "anchor" }] : []));
13281
13434
  field = viewChild('field', ...(ngDevMode ? [{ debugName: "field" }] : []));
13282
13435
  panelEl = null;
13436
+ /** Body-appending, positioning, follow-on-scroll and teardown — see SdsOverlayPanel. */
13437
+ overlay = new SdsOverlayPanel();
13283
13438
  isDisabled = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
13284
13439
  /** Countries after applying [onlyCountries] and [preferredCountries] ordering. */
13285
13440
  availableCountries = computed(() => {
@@ -13413,8 +13568,7 @@ class SdsPhoneInput {
13413
13568
  this.open.set(true);
13414
13569
  afterNextRender(() => {
13415
13570
  this.panelEl = this.host.nativeElement.querySelector('[data-sds-phone-panel]');
13416
- this.positionPanel();
13417
- this.addReflowListeners();
13571
+ this.attachPanel();
13418
13572
  this.panelEl?.querySelector('input')?.focus();
13419
13573
  this.scrollHighlightedIntoView();
13420
13574
  }, { injector: this.injector });
@@ -13422,8 +13576,7 @@ class SdsPhoneInput {
13422
13576
  close(returnFocus = false) {
13423
13577
  if (!this.open())
13424
13578
  return;
13425
- this.removeReflowListeners();
13426
- this.panelEl?.remove();
13579
+ this.overlay.detach();
13427
13580
  this.panelEl = null;
13428
13581
  this.open.set(false);
13429
13582
  if (returnFocus)
@@ -13444,7 +13597,7 @@ class SdsPhoneInput {
13444
13597
  onFilter(event) {
13445
13598
  this.filterText.set(event.target.value);
13446
13599
  this.highlightedIndex.set(this.filteredCountries().length ? 0 : -1);
13447
- setTimeout(() => this.positionPanel());
13600
+ setTimeout(() => this.overlay.reposition());
13448
13601
  }
13449
13602
  // --- keyboard ---
13450
13603
  onTriggerKeydown(event) {
@@ -13514,46 +13667,12 @@ class SdsPhoneInput {
13514
13667
  ?.scrollIntoView({ block: 'nearest' });
13515
13668
  });
13516
13669
  }
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);
13670
+ /** Attach once the panel exists — it is rendered inside an `@if`. */
13671
+ attachPanel() {
13672
+ const trigger = this.anchor()?.nativeElement;
13673
+ if (trigger && this.panelEl) {
13674
+ this.overlay.attach(trigger, this.panelEl, { width: 'min' });
13554
13675
  }
13555
- panelEl.style.top = `${Math.round(top)}px`;
13556
- panelEl.style.left = `${Math.round(left)}px`;
13557
13676
  }
13558
13677
  onDocumentClick(event) {
13559
13678
  if (!this.open())
@@ -13569,8 +13688,7 @@ class SdsPhoneInput {
13569
13688
  this.onTouched();
13570
13689
  }
13571
13690
  ngOnDestroy() {
13572
- this.removeReflowListeners();
13573
- this.panelEl?.remove();
13691
+ this.overlay.detach();
13574
13692
  }
13575
13693
  emit() {
13576
13694
  const value = this.e164();
@@ -13667,7 +13785,9 @@ class SdsPhoneInput {
13667
13785
  (click)="toggle()"
13668
13786
  (keydown)="onTriggerKeydown($event)"
13669
13787
  >
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">
13788
+ <span
13789
+ class="grid h-5 min-w-7 place-items-center rounded bg-muted px-1 text-[11px] font-semibold tracking-wide text-ink-soft"
13790
+ >
13671
13791
  {{ country().iso2 }}
13672
13792
  </span>
13673
13793
  <span class="text-ink-soft">+{{ country().dialCode }}</span>
@@ -13718,8 +13838,14 @@ class SdsPhoneInput {
13718
13838
  class="fixed z-[1000] overflow-hidden rounded-card border border-border bg-surface shadow-lg"
13719
13839
  >
13720
13840
  <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" />
13841
+ <div
13842
+ class="relative flex items-center rounded-control border border-border focus-within:border-brand-500"
13843
+ >
13844
+ <sds-icon
13845
+ name="search"
13846
+ [size]="16"
13847
+ class="pointer-events-none absolute left-2.5 text-ink-faint"
13848
+ />
13723
13849
  <input
13724
13850
  type="text"
13725
13851
  [attr.aria-controls]="listId"
@@ -13744,7 +13870,9 @@ class SdsPhoneInput {
13744
13870
  (click)="selectCountry(c)"
13745
13871
  (mouseenter)="highlightedIndex.set(i)"
13746
13872
  >
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">
13873
+ <span
13874
+ class="grid h-5 min-w-7 place-items-center rounded bg-muted px-1 text-[11px] font-semibold tracking-wide text-ink-soft"
13875
+ >
13748
13876
  {{ c.iso2 }}
13749
13877
  </span>
13750
13878
  <span class="flex-1 truncate">{{ countryName(c) }}</span>
@@ -13808,7 +13936,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
13808
13936
  (click)="toggle()"
13809
13937
  (keydown)="onTriggerKeydown($event)"
13810
13938
  >
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">
13939
+ <span
13940
+ class="grid h-5 min-w-7 place-items-center rounded bg-muted px-1 text-[11px] font-semibold tracking-wide text-ink-soft"
13941
+ >
13812
13942
  {{ country().iso2 }}
13813
13943
  </span>
13814
13944
  <span class="text-ink-soft">+{{ country().dialCode }}</span>
@@ -13859,8 +13989,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
13859
13989
  class="fixed z-[1000] overflow-hidden rounded-card border border-border bg-surface shadow-lg"
13860
13990
  >
13861
13991
  <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" />
13992
+ <div
13993
+ class="relative flex items-center rounded-control border border-border focus-within:border-brand-500"
13994
+ >
13995
+ <sds-icon
13996
+ name="search"
13997
+ [size]="16"
13998
+ class="pointer-events-none absolute left-2.5 text-ink-faint"
13999
+ />
13864
14000
  <input
13865
14001
  type="text"
13866
14002
  [attr.aria-controls]="listId"
@@ -13885,7 +14021,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
13885
14021
  (click)="selectCountry(c)"
13886
14022
  (mouseenter)="highlightedIndex.set(i)"
13887
14023
  >
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">
14024
+ <span
14025
+ class="grid h-5 min-w-7 place-items-center rounded bg-muted px-1 text-[11px] font-semibold tracking-wide text-ink-soft"
14026
+ >
13889
14027
  {{ c.iso2 }}
13890
14028
  </span>
13891
14029
  <span class="flex-1 truncate">{{ countryName(c) }}</span>
@@ -14070,7 +14208,8 @@ class SdsAddressAutocomplete {
14070
14208
  seq = 0;
14071
14209
  timer;
14072
14210
  destroyed = false;
14073
- panelEl = null;
14211
+ /** Body-appending, positioning, follow-on-scroll and teardown — see SdsOverlayPanel. */
14212
+ overlay = new SdsOverlayPanel();
14074
14213
  fieldId = computed(() => this.inputId() || this.autoId, ...(ngDevMode ? [{ debugName: "fieldId" }] : []));
14075
14214
  isDisabled = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
14076
14215
  pill = computed(() => this.shape() === 'pill', ...(ngDevMode ? [{ debugName: "pill" }] : []));
@@ -14333,79 +14472,57 @@ class SdsAddressAutocomplete {
14333
14472
  }
14334
14473
  scrollHighlightedIntoView() {
14335
14474
  setTimeout(() => {
14336
- const root = this.panelEl ?? this.host.nativeElement;
14475
+ const root = this.overlay.element ?? this.host.nativeElement;
14337
14476
  root
14338
14477
  .querySelector(`[data-index="${this.highlighted()}"]`)
14339
14478
  ?.scrollIntoView?.({ block: 'nearest' });
14340
14479
  });
14341
14480
  }
14342
14481
  // --- overlay ----------------------------------------------------------------
14343
- /** Close the overlay (a no-op `inline`, where the rows are always shown). */
14344
- close() {
14345
- this.open.set(false);
14346
- this.highlighted.set(-1);
14347
- this.detachPanel();
14348
- }
14349
- reflow = () => this.positionPanel();
14350
- listening = false;
14482
+ /**
14483
+ * Keep the body-appended panel in step with whether it should be shown at all.
14484
+ *
14485
+ * <p>Driven from `afterEveryRender`, so it runs on every change and must be cheap and idempotent
14486
+ * — attaching once, then repositioning while it stays open.
14487
+ */
14351
14488
  syncPanel() {
14352
14489
  if (this.inline() || !this.panelShown()) {
14353
- if (this.panelEl)
14354
- this.detachPanel();
14490
+ this.overlay.detach();
14355
14491
  return;
14356
14492
  }
14357
- if (!this.panelEl) {
14358
- this.panelEl = this.host.nativeElement.querySelector('[data-sds-address-panel]');
14359
- if (!this.panelEl)
14360
- return;
14361
- document.body.appendChild(this.panelEl);
14362
- if (!this.listening) {
14363
- window.addEventListener('scroll', this.reflow, true);
14364
- window.addEventListener('resize', this.reflow);
14365
- this.listening = true;
14366
- }
14367
- }
14368
- this.positionPanel();
14369
- }
14370
- detachPanel() {
14371
- if (this.listening) {
14372
- window.removeEventListener('scroll', this.reflow, true);
14373
- window.removeEventListener('resize', this.reflow);
14374
- this.listening = false;
14375
- }
14376
- this.panelEl?.remove();
14377
- this.panelEl = null;
14378
- }
14379
- positionPanel() {
14380
- const fieldEl = this.fieldRef()?.nativeElement;
14381
- const panelEl = this.panelEl;
14382
- if (!fieldEl || !panelEl)
14493
+ const field = this.fieldRef()?.nativeElement;
14494
+ if (!field)
14495
+ return;
14496
+ if (this.overlay.element) {
14497
+ this.overlay.reposition();
14383
14498
  return;
14384
- const r = fieldEl.getBoundingClientRect();
14385
- const gap = 4;
14386
- const margin = 8;
14387
- panelEl.style.width = `${Math.round(r.width)}px`;
14388
- const ph = panelEl.offsetHeight || 240;
14389
- let top = r.bottom + gap;
14390
- if (top + ph > window.innerHeight - margin && r.top - gap - ph > margin) {
14391
- top = r.top - gap - ph;
14392
14499
  }
14393
- panelEl.style.top = `${Math.round(top)}px`;
14394
- panelEl.style.left = `${Math.round(Math.max(margin, r.left))}px`;
14500
+ const panel = this.host.nativeElement.querySelector('[data-sds-address-panel]');
14501
+ if (!panel)
14502
+ return;
14503
+ // `exact`, not `min`: a suggestion list wider than the input it hangs off stops reading as
14504
+ // part of the field.
14505
+ this.overlay.attach(field, panel, { width: 'exact' });
14506
+ }
14507
+ /** Close the overlay (a no-op `inline`, where the rows are always shown). */
14508
+ close() {
14509
+ this.open.set(false);
14510
+ this.highlighted.set(-1);
14511
+ this.overlay.detach();
14395
14512
  }
14396
14513
  onDocumentClick(event) {
14397
14514
  if (this.inline() || !this.open())
14398
14515
  return;
14399
14516
  const target = event.target;
14400
14517
  const inField = this.host.nativeElement.contains(target);
14401
- const inPanel = !!this.panelEl && this.panelEl.contains(target);
14518
+ const inPanel = this.overlay.contains(target);
14402
14519
  if (!inField && !inPanel)
14403
14520
  this.close();
14404
14521
  }
14405
14522
  ngOnDestroy() {
14406
14523
  this.destroyed = true;
14407
14524
  clearTimeout(this.timer);
14408
- this.detachPanel();
14525
+ this.overlay.detach();
14409
14526
  }
14410
14527
  // --- ControlValueAccessor -----------------------------------------------------
14411
14528
  onChange = () => { };
@@ -14512,7 +14629,7 @@ class SdsAddressAutocomplete {
14512
14629
  <ng-container [ngTemplateOutlet]="panelBody" />
14513
14630
  </div>
14514
14631
  } @else if (panelShown()) {
14515
- <!-- Appended to document.body on open (see syncPanel). -->
14632
+ <!-- Appended to document.body on open (see SdsOverlayPanel). -->
14516
14633
  <div
14517
14634
  data-sds-address-panel
14518
14635
  class="fixed z-[1000] max-h-80 overflow-auto rounded-card border border-border bg-surface p-1 shadow-lg"
@@ -14737,7 +14854,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
14737
14854
  <ng-container [ngTemplateOutlet]="panelBody" />
14738
14855
  </div>
14739
14856
  } @else if (panelShown()) {
14740
- <!-- Appended to document.body on open (see syncPanel). -->
14857
+ <!-- Appended to document.body on open (see SdsOverlayPanel). -->
14741
14858
  <div
14742
14859
  data-sds-address-panel
14743
14860
  class="fixed z-[1000] max-h-80 overflow-auto rounded-card border border-border bg-surface p-1 shadow-lg"
@@ -21107,7 +21224,7 @@ let nextId = 0;
21107
21224
  * disclosure mechanics (closes on outside click, on Escape, and on selection, restoring focus to the trigger),
21108
21225
  * and the same panel styling and open animation — so the three top-bar controls read as one set.
21109
21226
  *
21110
- * The panel is appended to `document.body` while open and positioned against the trigger (like sds-select),
21227
+ * The panel is appended to `document.body` while open and placed by `SdsOverlayPanel` (like sds-select),
21111
21228
  * so a clipping ancestor — a bar with `overflow:hidden`, or a `glass` bar whose `backdrop-filter` is a
21112
21229
  * containing block — can never cut it off or paint over it. It repositions on scroll/resize and is removed
21113
21230
  * on close. Accessibility follows the APG select-only combobox: focus stays on the trigger, and Arrow/Home/
@@ -21148,7 +21265,8 @@ class SdsLanguageSwitcher {
21148
21265
  openChange = output();
21149
21266
  open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : []));
21150
21267
  highlightedIndex = signal(-1, ...(ngDevMode ? [{ debugName: "highlightedIndex" }] : []));
21151
- panelEl = null;
21268
+ /** Body-appending, positioning, follow-on-scroll and teardown — see SdsOverlayPanel. */
21269
+ overlay = new SdsOverlayPanel();
21152
21270
  /** Normalise the mixed `languages` input to `{ code, label }`, defaulting the label to the code. */
21153
21271
  options = computed(() => this.languages().map((l) => typeof l === 'string'
21154
21272
  ? { code: l, label: l.toUpperCase() }
@@ -21175,9 +21293,17 @@ class SdsLanguageSwitcher {
21175
21293
  // clipping/stacking ancestor (an overflow:hidden bar, a backdrop-filter glass bar) can cut it off.
21176
21294
  // Focus stays on the trigger (combobox model); the active row is surfaced via aria-activedescendant.
21177
21295
  afterNextRender(() => {
21178
- this.panelEl = this.el.nativeElement.querySelector('[data-sds-language-panel]');
21179
- this.positionPanel();
21180
- this.addReflowListeners();
21296
+ const panel = this.el.nativeElement.querySelector('[data-sds-language-panel]');
21297
+ const trigger = this.trigger()?.nativeElement;
21298
+ if (panel && trigger) {
21299
+ // gap 8 and an end-aligned panel, as this control has always used; `direction="up"`
21300
+ // becomes the shared `prefer: 'above'`.
21301
+ this.overlay.attach(trigger, panel, {
21302
+ gap: 8,
21303
+ align: this.align(),
21304
+ prefer: this.direction() === 'up' ? 'above' : 'below',
21305
+ });
21306
+ }
21181
21307
  this.trigger()?.nativeElement.focus();
21182
21308
  this.scrollHighlightedIntoView();
21183
21309
  }, { injector: this.injector });
@@ -21185,10 +21311,8 @@ class SdsLanguageSwitcher {
21185
21311
  close(returnFocus = false) {
21186
21312
  if (!this.open())
21187
21313
  return;
21188
- this.removeReflowListeners();
21189
21314
  // Remove the portalled node ourselves before Angular tears the view down (it lives in <body> now).
21190
- this.panelEl?.remove();
21191
- this.panelEl = null;
21315
+ this.overlay.detach();
21192
21316
  this.open.set(false);
21193
21317
  this.openChange.emit(false);
21194
21318
  if (returnFocus)
@@ -21204,7 +21328,7 @@ class SdsLanguageSwitcher {
21204
21328
  return;
21205
21329
  const target = event.target;
21206
21330
  const inHost = this.el.nativeElement.contains(target);
21207
- const inPanel = !!this.panelEl && this.panelEl.contains(target);
21331
+ const inPanel = this.overlay.contains(target);
21208
21332
  if (!inHost && !inPanel)
21209
21333
  this.close();
21210
21334
  }
@@ -21251,7 +21375,8 @@ class SdsLanguageSwitcher {
21251
21375
  case 'Tab':
21252
21376
  this.close(false);
21253
21377
  break;
21254
- default: break;
21378
+ default:
21379
+ break;
21255
21380
  }
21256
21381
  }
21257
21382
  initHighlight() {
@@ -21278,68 +21403,19 @@ class SdsLanguageSwitcher {
21278
21403
  }
21279
21404
  scrollHighlightedIntoView() {
21280
21405
  setTimeout(() => {
21281
- this.panelEl
21406
+ this.overlay.element
21282
21407
  ?.querySelector(`[data-index="${this.highlightedIndex()}"]`)
21283
21408
  ?.scrollIntoView({ block: 'nearest' });
21284
21409
  });
21285
21410
  }
21286
- // --- body-appended overlay positioning (mirrors sds-select / sds-date-picker) ---
21287
- reflow = () => this.positionPanel();
21288
- listening = false;
21289
- addReflowListeners() {
21290
- if (this.listening)
21291
- return;
21292
- window.addEventListener('scroll', this.reflow, true);
21293
- window.addEventListener('resize', this.reflow);
21294
- this.listening = true;
21295
- }
21296
- removeReflowListeners() {
21297
- if (!this.listening)
21298
- return;
21299
- window.removeEventListener('scroll', this.reflow, true);
21300
- window.removeEventListener('resize', this.reflow);
21301
- this.listening = false;
21302
- }
21303
- positionPanel() {
21304
- const triggerEl = this.trigger()?.nativeElement;
21305
- const panelEl = this.panelEl;
21306
- if (!triggerEl || !panelEl)
21307
- return;
21308
- if (panelEl.parentElement !== document.body)
21309
- document.body.appendChild(panelEl);
21310
- const r = triggerEl.getBoundingClientRect();
21311
- const gap = 8;
21312
- const margin = 8;
21313
- const pw = panelEl.offsetWidth || 176;
21314
- const ph = panelEl.offsetHeight || 200;
21315
- // Vertical: below by default; open above when asked (direction='up') and there is room, or when there
21316
- // isn't room below but there is above. The panel height is bounded by the list's max-h, but clamp
21317
- // defensively so top can never be pushed off the top edge.
21318
- const roomBelow = window.innerHeight - r.bottom - gap;
21319
- const roomAbove = r.top - gap;
21320
- let top = (this.direction() === 'up' && roomAbove >= ph) || (roomBelow < ph && roomAbove > roomBelow)
21321
- ? r.top - gap - ph
21322
- : r.bottom + gap;
21323
- if (top + ph > window.innerHeight - margin)
21324
- top = window.innerHeight - margin - ph;
21325
- if (top < margin)
21326
- top = margin;
21327
- // Horizontal: align the panel's end (right) or start (left) edge to the trigger, then clamp.
21328
- let left = this.align() === 'end' ? r.right - pw : r.left;
21329
- left = Math.min(left, window.innerWidth - margin - pw);
21330
- left = Math.max(margin, left);
21331
- panelEl.style.top = `${Math.round(top)}px`;
21332
- panelEl.style.left = `${Math.round(left)}px`;
21333
- }
21334
21411
  ngOnDestroy() {
21335
- this.removeReflowListeners();
21336
- this.panelEl?.remove();
21412
+ this.overlay.detach();
21337
21413
  }
21338
21414
  onBrand = computed(() => this.appearance() === 'on-brand', ...(ngDevMode ? [{ debugName: "onBrand" }] : []));
21339
21415
  triggerClass = computed(() => this.onBrand()
21340
21416
  ? 'text-white/85 hover:bg-white/10 hover:text-white focus-visible:ring-white/40'
21341
21417
  : 'bg-muted text-ink-soft hover:bg-brand-50 hover:text-brand-600 focus-visible:ring-brand-500/40', ...(ngDevMode ? [{ debugName: "triggerClass" }] : []));
21342
- chevronClass = computed(() => (this.onBrand() ? 'text-white/70' : 'text-ink-faint'), ...(ngDevMode ? [{ debugName: "chevronClass" }] : []));
21418
+ chevronClass = computed(() => this.onBrand() ? 'text-white/70' : 'text-ink-faint', ...(ngDevMode ? [{ debugName: "chevronClass" }] : []));
21343
21419
  rowClass(index, selected) {
21344
21420
  const base = 'flex w-full cursor-pointer items-center gap-3 px-4 py-2.5 text-sm font-medium transition-colors';
21345
21421
  const highlighted = this.highlightedIndex() === index;
@@ -21380,7 +21456,7 @@ class SdsLanguageSwitcher {
21380
21456
  </button>
21381
21457
 
21382
21458
  @if (open()) {
21383
- <!-- Appended to document.body on open (see positionPanel) so no ancestor can clip it. -->
21459
+ <!-- Appended to document.body on open (see SdsOverlayPanel) so no ancestor can clip it. -->
21384
21460
  <div
21385
21461
  data-sds-language-panel
21386
21462
  [id]="panelId"
@@ -21389,7 +21465,9 @@ class SdsLanguageSwitcher {
21389
21465
  class="fixed z-[1000] min-w-44 overflow-hidden rounded-card border border-border bg-surface shadow-xl animate-sds-menu"
21390
21466
  >
21391
21467
  @if (heading(); as h) {
21392
- <p class="border-b border-border px-4 pb-2 pt-3 text-[0.68rem] font-semibold uppercase tracking-wider text-ink-faint">
21468
+ <p
21469
+ class="border-b border-border px-4 pb-2 pt-3 text-[0.68rem] font-semibold uppercase tracking-wider text-ink-faint"
21470
+ >
21393
21471
  {{ h }}
21394
21472
  </p>
21395
21473
  }
@@ -21455,7 +21533,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
21455
21533
  </button>
21456
21534
 
21457
21535
  @if (open()) {
21458
- <!-- Appended to document.body on open (see positionPanel) so no ancestor can clip it. -->
21536
+ <!-- Appended to document.body on open (see SdsOverlayPanel) so no ancestor can clip it. -->
21459
21537
  <div
21460
21538
  data-sds-language-panel
21461
21539
  [id]="panelId"
@@ -21464,7 +21542,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
21464
21542
  class="fixed z-[1000] min-w-44 overflow-hidden rounded-card border border-border bg-surface shadow-xl animate-sds-menu"
21465
21543
  >
21466
21544
  @if (heading(); as h) {
21467
- <p class="border-b border-border px-4 pb-2 pt-3 text-[0.68rem] font-semibold uppercase tracking-wider text-ink-faint">
21545
+ <p
21546
+ class="border-b border-border px-4 pb-2 pt-3 text-[0.68rem] font-semibold uppercase tracking-wider text-ink-faint"
21547
+ >
21468
21548
  {{ h }}
21469
21549
  </p>
21470
21550
  }
@@ -21651,5 +21731,5 @@ function provideSdsCharts(config) {
21651
21731
  * Generated bundle index. Do not edit.
21652
21732
  */
21653
21733
 
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 };
21734
+ 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
21735
  //# sourceMappingURL=besaitech-ng-design-system.mjs.map