@lucca-front/ng 22.0.5 → 22.1.0-rc.2

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.
Files changed (76) hide show
  1. package/fesm2022/lucca-front-ng-activity-feed.mjs +12 -9
  2. package/fesm2022/lucca-front-ng-activity-feed.mjs.map +1 -1
  3. package/fesm2022/lucca-front-ng-approbation-inbox.mjs +1 -1
  4. package/fesm2022/lucca-front-ng-approbation-inbox.mjs.map +1 -1
  5. package/fesm2022/lucca-front-ng-callout.mjs +1 -1
  6. package/fesm2022/lucca-front-ng-callout.mjs.map +1 -1
  7. package/fesm2022/lucca-front-ng-core-select.mjs +236 -8
  8. package/fesm2022/lucca-front-ng-core-select.mjs.map +1 -1
  9. package/fesm2022/lucca-front-ng-core.mjs +5 -1
  10. package/fesm2022/lucca-front-ng-core.mjs.map +1 -1
  11. package/fesm2022/lucca-front-ng-data-table.mjs +2 -2
  12. package/fesm2022/lucca-front-ng-data-table.mjs.map +1 -1
  13. package/fesm2022/lucca-front-ng-date2.mjs +217 -159
  14. package/fesm2022/lucca-front-ng-date2.mjs.map +1 -1
  15. package/fesm2022/lucca-front-ng-dialog.mjs +2 -2
  16. package/fesm2022/lucca-front-ng-dialog.mjs.map +1 -1
  17. package/fesm2022/lucca-front-ng-fancy-box.mjs +47 -24
  18. package/fesm2022/lucca-front-ng-fancy-box.mjs.map +1 -1
  19. package/fesm2022/lucca-front-ng-file-upload.mjs +1 -1
  20. package/fesm2022/lucca-front-ng-file-upload.mjs.map +1 -1
  21. package/fesm2022/lucca-front-ng-filter-pills-core.mjs +55 -0
  22. package/fesm2022/lucca-front-ng-filter-pills-core.mjs.map +1 -0
  23. package/fesm2022/lucca-front-ng-filter-pills.mjs +74 -69
  24. package/fesm2022/lucca-front-ng-filter-pills.mjs.map +1 -1
  25. package/fesm2022/lucca-front-ng-form-field.mjs +37 -5
  26. package/fesm2022/lucca-front-ng-form-field.mjs.map +1 -1
  27. package/fesm2022/lucca-front-ng-form-label.mjs +1 -1
  28. package/fesm2022/lucca-front-ng-form-label.mjs.map +1 -1
  29. package/fesm2022/lucca-front-ng-form.mjs +42 -5
  30. package/fesm2022/lucca-front-ng-form.mjs.map +1 -1
  31. package/fesm2022/lucca-front-ng-forms-rich-text-input.mjs +3 -3
  32. package/fesm2022/lucca-front-ng-forms-rich-text-input.mjs.map +1 -1
  33. package/fesm2022/lucca-front-ng-forms.mjs +12 -6
  34. package/fesm2022/lucca-front-ng-forms.mjs.map +1 -1
  35. package/fesm2022/lucca-front-ng-highlight-data.mjs +6 -7
  36. package/fesm2022/lucca-front-ng-highlight-data.mjs.map +1 -1
  37. package/fesm2022/lucca-front-ng-impersonation.mjs +19 -4
  38. package/fesm2022/lucca-front-ng-impersonation.mjs.map +1 -1
  39. package/fesm2022/lucca-front-ng-link.mjs +8 -3
  40. package/fesm2022/lucca-front-ng-link.mjs.map +1 -1
  41. package/fesm2022/lucca-front-ng-markdown.mjs +51 -0
  42. package/fesm2022/lucca-front-ng-markdown.mjs.map +1 -0
  43. package/fesm2022/lucca-front-ng-multi-select.mjs +147 -43
  44. package/fesm2022/lucca-front-ng-multi-select.mjs.map +1 -1
  45. package/fesm2022/lucca-front-ng-popover2.mjs +11 -5
  46. package/fesm2022/lucca-front-ng-popover2.mjs.map +1 -1
  47. package/fesm2022/lucca-front-ng-priority-levels.mjs +128 -0
  48. package/fesm2022/lucca-front-ng-priority-levels.mjs.map +1 -0
  49. package/fesm2022/lucca-front-ng-safe-content.mjs +5 -2
  50. package/fesm2022/lucca-front-ng-safe-content.mjs.map +1 -1
  51. package/fesm2022/lucca-front-ng-simple-select.mjs +63 -12
  52. package/fesm2022/lucca-front-ng-simple-select.mjs.map +1 -1
  53. package/fesm2022/lucca-front-ng-software-icon-wrapper.mjs +1 -1
  54. package/fesm2022/lucca-front-ng-software-icon-wrapper.mjs.map +1 -1
  55. package/fesm2022/lucca-front-ng-user-popover.mjs +3 -2
  56. package/fesm2022/lucca-front-ng-user-popover.mjs.map +1 -1
  57. package/package.json +17 -4
  58. package/src/components/cdk/_overlay.scss +3 -1
  59. package/types/lucca-front-ng-activity-feed.d.ts +3 -1
  60. package/types/lucca-front-ng-core-select.d.ts +90 -7
  61. package/types/lucca-front-ng-core.d.ts +3 -1
  62. package/types/lucca-front-ng-date2.d.ts +31 -5
  63. package/types/lucca-front-ng-fancy-box.d.ts +29 -12
  64. package/types/lucca-front-ng-filter-pills-core.d.ts +69 -0
  65. package/types/lucca-front-ng-filter-pills.d.ts +28 -55
  66. package/types/lucca-front-ng-form-field.d.ts +12 -4
  67. package/types/lucca-front-ng-form.d.ts +7 -1
  68. package/types/lucca-front-ng-forms.d.ts +7 -2
  69. package/types/lucca-front-ng-highlight-data.d.ts +1 -2
  70. package/types/lucca-front-ng-impersonation.d.ts +14 -1
  71. package/types/lucca-front-ng-link.d.ts +5 -1
  72. package/types/lucca-front-ng-markdown.d.ts +24 -0
  73. package/types/lucca-front-ng-multi-select.d.ts +10 -1
  74. package/types/lucca-front-ng-popover2.d.ts +7 -1
  75. package/types/lucca-front-ng-priority-levels.d.ts +44 -0
  76. package/types/lucca-front-ng-simple-select.d.ts +4 -0
@@ -1,9 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, Directive, InjectionToken, inject, ChangeDetectionStrategy, Component, TemplateRef, Pipe, ViewContainerRef, Injector, afterNextRender, DestroyRef, computed, afterRenderEffect, untracked, Injectable, ElementRef, signal, model, linkedSignal, output, contentChild, viewChild, ChangeDetectorRef, forwardRef, booleanAttribute, ViewEncapsulation, effect, EventEmitter, DOCUMENT, NgZone } from '@angular/core';
3
- import { syncInputSignal, ɵeffectWithDeps as _effectWithDeps, isNotNil, luBooleanAttribute, intlInputOptions, luOptionalNumberAttribute, isNil, PortalDirective, luNumberAttribute } from '@lucca-front/ng/core';
2
+ import { input, Directive, InjectionToken, inject, ChangeDetectionStrategy, Component, TemplateRef, Pipe, ViewContainerRef, Injector, afterNextRender, DestroyRef, afterRenderEffect, untracked, computed, Injectable, ElementRef, signal, model, linkedSignal, output, contentChild, viewChild, ChangeDetectorRef, forwardRef, booleanAttribute, ViewEncapsulation, effect, EventEmitter, ApplicationRef, DOCUMENT, NgZone } from '@angular/core';
3
+ import { syncInputSignal, ɵeffectWithDeps as _effectWithDeps, isNotNil, luBooleanAttribute, intlInputOptions, luOptionalNumberAttribute, isNil, PortalDirective, luNullableNumberAttribute, injectMediaMinBreakpoint, luNumberAttribute } from '@lucca-front/ng/core';
4
4
  import { OverlayContainer } from '@angular/cdk/overlay';
5
5
  import { toObservable, toSignal, takeUntilDestroyed, outputFromObservable } from '@angular/core/rxjs-interop';
6
- import { FILTER_PILL_INPUT_COMPONENT, FILTER_PILL_HOST_COMPONENT } from '@lucca-front/ng/filter-pills';
6
+ import { FILTER_PILL_INPUT_COMPONENT, FILTER_PILL_HOST_COMPONENT } from '@lucca-front/ng/filter-pills/core';
7
+ import { FORM_FIELD_INSTANCE } from '@lucca-front/ng/form-field';
7
8
  import { BehaviorSubject, skip, take, map, startWith, switchMap, of, distinctUntilChanged, observeOn, asyncScheduler, filter, EMPTY, timer, defer, catchError, defaultIfEmpty, tap, expand, scan, takeWhile, finalize, ReplaySubject, Subject } from 'rxjs';
8
9
  import { AsyncPipe, NgTemplateOutlet } from '@angular/common';
9
10
  import { Treeitem, OptionComponent } from '@lucca-front/ng/listbox';
@@ -268,6 +269,53 @@ function getGroupTemplateLocation(hasGrouping, clue, options, searchable = true)
268
269
  }));
269
270
  return toSignal(location$, { initialValue: hasGrouping() ? 'group-header' : 'none' });
270
271
  }
272
+ /**
273
+ * Requests the next page for as long as the panel's scroll viewport can't scroll.
274
+ *
275
+ * Pagination is driven by the scroll event: reaching the bottom of the viewport asks the consumer
276
+ * for the next page. A page that doesn't overflow the viewport therefore never asks for anything —
277
+ * there is nothing to scroll. The popover panel caps its content at 20rem, which a page of options
278
+ * always overflows, so this never came up; the bottom sheet is only as tall as its content instead,
279
+ * so a first page of a few options leaves it short of a scrollbar and pagination stops right there.
280
+ *
281
+ * So the viewport is measured again after every render and, while it still can't scroll, the next
282
+ * page is requested. The option count is what guards against an endless loop: a page that brought
283
+ * no new option — the data source is exhausted — is never requested twice. A new search landing on
284
+ * exactly the same count as the last requested page is the one case left unfilled, which the next
285
+ * scroll — or the next keystroke — resolves on its own.
286
+ *
287
+ * The request itself is deferred by a microtask: the panel's first render happens inside the sheet's
288
+ * own `ApplicationRef.tick()`, which `openPanel` runs *before* subscribing the select input to the
289
+ * panel ref, so a page requested straight from that render would be emitted into the void. A
290
+ * microtask lands once `openPanel` has returned, still well ahead of anything the user could do.
291
+ *
292
+ * Must be called from an injection context.
293
+ */
294
+ function fillScrollViewport({ viewport, optionCount, loading, nextPage, }) {
295
+ let lastRequestedCount = null;
296
+ let destroyed = false;
297
+ inject(DestroyRef).onDestroy(() => (destroyed = true));
298
+ afterRenderEffect({
299
+ read: () => {
300
+ const element = viewport();
301
+ const count = optionCount();
302
+ if (!element || loading() || count === 0 || count === lastRequestedCount) {
303
+ return;
304
+ }
305
+ // `scrollHeight` and `clientHeight` are both rounded, so a sub-pixel difference between
306
+ // them isn't a scrollbar: the same 1px tolerance as the panels' scroll handler applies.
307
+ if (element.scrollHeight - element.clientHeight > 1) {
308
+ return;
309
+ }
310
+ lastRequestedCount = count;
311
+ queueMicrotask(() => {
312
+ if (!destroyed) {
313
+ untracked(nextPage);
314
+ }
315
+ });
316
+ },
317
+ });
318
+ }
271
319
 
272
320
  class CoreSelectKeyManager {
273
321
  #destroyRef = inject(DestroyRef);
@@ -675,6 +723,15 @@ function accumulatedPages(ds, clue, { nextPage$, setLoading }) {
675
723
  const coreSelectDefaultOptionComparer = (option1, option2) => JSON.stringify(option1) === JSON.stringify(option2);
676
724
  const coreSelectDefaultOptionKey = (option) => option;
677
725
  class ALuSelectInputComponent {
726
+ // Signal-backed because a filter pill takes the select over *after* it has rendered, and
727
+ // `bottomSheetMode` — which gates the trigger element itself — is computed from it.
728
+ #filterPillMode;
729
+ get filterPillMode() {
730
+ return this.#filterPillMode();
731
+ }
732
+ set filterPillMode(filterPillMode) {
733
+ this.#filterPillMode.set(filterPillMode);
734
+ }
678
735
  #defaultFilterPillClearable;
679
736
  #defaultClearable;
680
737
  get searchable() {
@@ -751,12 +808,24 @@ class ALuSelectInputComponent {
751
808
  this.overlayContainerRef = inject(OverlayContainer).getContainerElement();
752
809
  this.labelElement = inject(SELECT_LABEL);
753
810
  this.labelId = inject(SELECT_LABEL_ID);
811
+ this.formField = inject(FORM_FIELD_INSTANCE, { optional: true });
754
812
  this.filterPillHost = inject(FILTER_PILL_HOST_COMPONENT, { optional: true });
755
- this.filterPillMode = false;
813
+ // Signal-backed because a filter pill takes the select over *after* it has rendered, and
814
+ // `bottomSheetMode` — which gates the trigger element itself — is computed from it.
815
+ this.#filterPillMode = signal(false, /* @ts-ignore */
816
+ ...(ngDevMode ? [{ debugName: "#filterPillMode" }] : /* istanbul ignore next */ []));
756
817
  this.ignorePresentation = input(false, { ...(ngDevMode ? { debugName: "ignorePresentation" } : /* istanbul ignore next */ {}), transform: luBooleanAttribute });
818
+ /**
819
+ * Width of the select itself, using the same scale as the form field width
820
+ */
821
+ this.width = input(null, { ...(ngDevMode ? { debugName: "width" } : /* istanbul ignore next */ {}), transform: (luNullableNumberAttribute) });
822
+ this.widthClass = computed(() => (this.width() ? `mod-width${this.width()}` : null), /* @ts-ignore */
823
+ ...(ngDevMode ? [{ debugName: "widthClass" }] : /* istanbul ignore next */ []));
757
824
  this.panelClosed = output();
758
825
  this.panelOpened = output();
759
826
  this.highlightedOption = output();
827
+ // Below the `S` breakpoint the trigger is a `<button>` rather than the text input, so this is only
828
+ // ever focused, never read as an input.
760
829
  this.inputElementRef = viewChild('inputElement', /* @ts-ignore */
761
830
  ...(ngDevMode ? [{ debugName: "inputElementRef" }] : /* istanbul ignore next */ []));
762
831
  this.disabled$ = new BehaviorSubject(false);
@@ -779,6 +848,29 @@ class ALuSelectInputComponent {
779
848
  this.isPanelOpen$ = new BehaviorSubject(false);
780
849
  // TODO Might be temporary, check after merging signalize PR
781
850
  this.panelOpenSignal = toSignal(this.isPanelOpen$);
851
+ this.belowSmallBreakpoint = injectMediaMinBreakpoint('S', true);
852
+ /**
853
+ * Below the `S` breakpoint (800px) the panel opens as a dialog in `sheet` mode that embeds its own
854
+ * search input, instead of a popover anchored to the field — mirroring how filter pills move the input
855
+ * inside their overlay. Filter pills already provide their own overlay, so they keep their behavior.
856
+ */
857
+ this.bottomSheetMode = computed(() => (this.belowSmallBreakpoint() ?? false) && !this.filterPillMode, /* @ts-ignore */
858
+ ...(ngDevMode ? [{ debugName: "bottomSheetMode" }] : /* istanbul ignore next */ []));
859
+ /**
860
+ * Whether the currently open panel is a sheet, snapshotted when it opens rather than tracking
861
+ * `bottomSheetMode()` live: the panel's own ref (`SelectPanelSheetRef` vs `SelectPanelRef`) is
862
+ * chosen once at open time too, so resizing across the breakpoint while the panel stays open must
863
+ * not flip which surface its template renders — that surface was never actually attached as a
864
+ * dialog, so switching to it crashes with a missing `LuDialogRef` provider.
865
+ */
866
+ this.panelBottomSheetMode = signal(false, /* @ts-ignore */
867
+ ...(ngDevMode ? [{ debugName: "panelBottomSheetMode" }] : /* istanbul ignore next */ []));
868
+ /** Field label echoed as the bottom sheet's title, snapshotted when the sheet opens. */
869
+ this.panelTitle = signal('', /* @ts-ignore */
870
+ ...(ngDevMode ? [{ debugName: "panelTitle" }] : /* istanbul ignore next */ []));
871
+ /** Whether that field label carries the required marker, so the sheet's title can echo it too. */
872
+ this.panelTitleRequired = computed(() => this.formField?.isInputRequired() ?? false, /* @ts-ignore */
873
+ ...(ngDevMode ? [{ debugName: "panelTitleRequired" }] : /* istanbul ignore next */ []));
782
874
  this.activeDescendant$ = new BehaviorSubject('');
783
875
  this.manualOptions$ = new ReplaySubject(1);
784
876
  this.overlayConfig = input({ hasBackdrop: true, backdropClass: 'cdk-overlay-transparent-backdrop' }, /* @ts-ignore */
@@ -854,6 +946,7 @@ class ALuSelectInputComponent {
854
946
  })));
855
947
  this.destroyed$ = new Subject();
856
948
  this.injector = inject(Injector);
949
+ this.hostElementRef = inject(ElementRef);
857
950
  // Re-entrancy guard: opening the panel (focus, clueChanged, overlay creation) can synchronously
858
951
  // trigger openPanel again, which used to cause a double tap.
859
952
  this.#isOpeningPanel = false;
@@ -865,6 +958,13 @@ class ALuSelectInputComponent {
865
958
  this.manualOptions$.next(options);
866
959
  }
867
960
  });
961
+ // Rather than leaving an open panel stuck in a stale sheet/overlay style once `bottomSheetMode()`
962
+ // no longer matches it, close it outright — the next open picks the right style for the new breakpoint.
963
+ _effectWithDeps([this.bottomSheetMode], (bottomSheetMode) => {
964
+ if (this.isPanelOpen && bottomSheetMode !== this.panelBottomSheetMode()) {
965
+ this.closePanel();
966
+ }
967
+ });
868
968
  // When options arrive asynchronously via a dataSource, dataSourceOptions changes but options doesn't.
869
969
  // We need to reposition the panel after the DOM updates in both cases.
870
970
  _effectWithDeps([this.dataSourceOptions], (_options, onCleanup) => {
@@ -894,6 +994,11 @@ class ALuSelectInputComponent {
894
994
  this.panelRef?.close();
895
995
  break;
896
996
  case 'Tab':
997
+ // A bottom sheet is modal: its focus trap cycles Tab between the sheet's own controls, so
998
+ // tabbing must not close it the way leaving the field does on desktop.
999
+ if (this.bottomSheetMode()) {
1000
+ break;
1001
+ }
897
1002
  // If we are in a filterpill, this will close it on tab press, but we want it to not lose any
898
1003
  // displayed stuff and properly close on focus exit
899
1004
  this.panelRef?.close();
@@ -976,7 +1081,17 @@ class ALuSelectInputComponent {
976
1081
  }
977
1082
  this.#isOpeningPanel = true;
978
1083
  try {
979
- this.focusInput();
1084
+ const isSheet = this.bottomSheetMode();
1085
+ this.panelBottomSheetMode.set(isSheet);
1086
+ if (isSheet) {
1087
+ // The sheet shows the field label as its title and embeds its own search input (auto-focused by
1088
+ // `openSelectPanelSheet`); focusing the covered host input would pop the mobile keyboard on a
1089
+ // hidden field.
1090
+ this.panelTitle.set(this.resolvePanelTitle());
1091
+ }
1092
+ else {
1093
+ this.focusInput();
1094
+ }
980
1095
  const isSearchable = this.searchable;
981
1096
  this.isPanelOpen$.next(true);
982
1097
  this.panelOpened.emit();
@@ -1011,6 +1126,31 @@ class ALuSelectInputComponent {
1011
1126
  this.inputElementRef()?.nativeElement.focus();
1012
1127
  }
1013
1128
  }
1129
+ // The bottom sheet echoes the field label as its title. Standalone selects may be wrapped in a <label>,
1130
+ // but inside a form-field the label is a separate element referenced through the control's aria-labelledby.
1131
+ resolvePanelTitle() {
1132
+ if (this.labelElement) {
1133
+ return this.getLabelText(this.labelElement);
1134
+ }
1135
+ // The field points its own `aria-labelledby` at its value displayer, so only an actual `<label>`
1136
+ // counts here — otherwise a select without any label would echo its selected value as the title.
1137
+ // `aria-labelledby` can list several ids (label, hint, …), and some can be stale — e.g. leftover
1138
+ // from before the trigger was torn down and rebuilt as the breakpoint flipped between the button
1139
+ // and input variants — so every id is checked instead of trusting the first one to be the label.
1140
+ const host = this.hostElementRef.nativeElement;
1141
+ const labelledByIds = host.querySelector('[aria-labelledby]')?.getAttribute('aria-labelledby')?.split(' ') ?? [];
1142
+ const label = labelledByIds.map((id) => host.ownerDocument.getElementById(id)).find((element) => element?.tagName === 'LABEL');
1143
+ return label ? this.getLabelText(label) : '';
1144
+ }
1145
+ // Read the label text without its adornments (help tooltip, required marker, screen-reader-only copy)
1146
+ // nor, when the label wraps the select, the field's own rendered content — its selected value or the
1147
+ // placeholder standing in for it — so the title stays the plain field label.
1148
+ getLabelText(label) {
1149
+ const clone = label.cloneNode(true);
1150
+ const selectTag = this.hostElementRef.nativeElement.tagName.toLowerCase();
1151
+ clone.querySelectorAll(`${selectTag}, [role="button"], .pr-u-mask, .formLabel-required`).forEach((node) => node.remove());
1152
+ return (clone.textContent ?? '').trim();
1153
+ }
1014
1154
  emptyClue() {
1015
1155
  if (this.clue) {
1016
1156
  this.clue = null;
@@ -1070,7 +1210,7 @@ class ALuSelectInputComponent {
1070
1210
  this.isPanelOpen$.next(false);
1071
1211
  }
1072
1212
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ALuSelectInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1073
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "22.1.4", type: ALuSelectInputComponent, isStandalone: true, inputs: { ignorePresentation: { classPropertyName: "ignorePresentation", publicName: "ignorePresentation", isSignal: true, isRequired: false, transformFunction: null }, prefix: { classPropertyName: "prefix", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, clearableInput: { classPropertyName: "clearableInput", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, addOptionLabelInput: { classPropertyName: "addOptionLabelInput", publicName: "addOptionLabel", isSignal: true, isRequired: false, transformFunction: null }, addOptionStrategy: { classPropertyName: "addOptionStrategy", publicName: "addOptionStrategy", isSignal: true, isRequired: false, transformFunction: null }, overlayConfig: { classPropertyName: "overlayConfig", publicName: "overlayConfig", isSignal: true, isRequired: false, transformFunction: null }, loadingInput: { classPropertyName: "loadingInput", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, optionsInput: { classPropertyName: "optionsInput", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, optionComparerInput: { classPropertyName: "optionComparerInput", publicName: "optionComparer", isSignal: true, isRequired: false, transformFunction: null }, optionKeyInput: { classPropertyName: "optionKeyInput", publicName: "optionKey", isSignal: true, isRequired: false, transformFunction: null }, noClueIcon: { classPropertyName: "noClueIcon", publicName: "noClueIcon", isSignal: true, isRequired: false, transformFunction: null }, inputTabindex: { classPropertyName: "inputTabindex", publicName: "inputTabindex", isSignal: true, isRequired: false, transformFunction: null }, compact: { classPropertyName: "compact", publicName: "compact", isSignal: true, isRequired: false, transformFunction: null }, colorPicker: { classPropertyName: "colorPicker", publicName: "colorPicker", isSignal: true, isRequired: false, transformFunction: null }, optionTpl: { classPropertyName: "optionTpl", publicName: "optionTpl", isSignal: true, isRequired: false, transformFunction: null }, valueTpl: { classPropertyName: "valueTpl", publicName: "valueTpl", isSignal: true, isRequired: false, transformFunction: null }, panelHeaderTpl: { classPropertyName: "panelHeaderTpl", publicName: "panelHeaderTpl", isSignal: true, isRequired: false, transformFunction: null }, panelFooterTpl: { classPropertyName: "panelFooterTpl", publicName: "panelFooterTpl", isSignal: true, isRequired: false, transformFunction: null }, dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { panelClosed: "panelClosed", panelOpened: "panelOpened", highlightedOption: "highlightedOption", optionTpl: "optionTplChange", valueTpl: "valueTplChange", panelHeaderTpl: "panelHeaderTplChange", panelFooterTpl: "panelFooterTplChange", clueChange: "clueChange", nextPage: "nextPage", addOption: "addOption", dataSource: "dataSourceChange" }, host: { listeners: { "click": "onClickOpenPanel($event)", "keydown": "onKeyDownNavigation($event)" }, properties: { "class.colorPicker": "colorPicker()", "class.mod-compact": "compact()", "class.is-clearable": "isClearable()", "class.is-selected": "isSelectedClass", "class.is-searchFilled": "isSearchFilledClass", "class.mod-noClueIcon": "isNoClueIconClass" } }, viewQueries: [{ propertyName: "inputElementRef", first: true, predicate: ["inputElement"], descendants: true, isSignal: true }], ngImport: i0 }); }
1213
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "22.1.4", type: ALuSelectInputComponent, isStandalone: true, inputs: { ignorePresentation: { classPropertyName: "ignorePresentation", publicName: "ignorePresentation", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, prefix: { classPropertyName: "prefix", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, clearableInput: { classPropertyName: "clearableInput", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, addOptionLabelInput: { classPropertyName: "addOptionLabelInput", publicName: "addOptionLabel", isSignal: true, isRequired: false, transformFunction: null }, addOptionStrategy: { classPropertyName: "addOptionStrategy", publicName: "addOptionStrategy", isSignal: true, isRequired: false, transformFunction: null }, overlayConfig: { classPropertyName: "overlayConfig", publicName: "overlayConfig", isSignal: true, isRequired: false, transformFunction: null }, loadingInput: { classPropertyName: "loadingInput", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, optionsInput: { classPropertyName: "optionsInput", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, optionComparerInput: { classPropertyName: "optionComparerInput", publicName: "optionComparer", isSignal: true, isRequired: false, transformFunction: null }, optionKeyInput: { classPropertyName: "optionKeyInput", publicName: "optionKey", isSignal: true, isRequired: false, transformFunction: null }, noClueIcon: { classPropertyName: "noClueIcon", publicName: "noClueIcon", isSignal: true, isRequired: false, transformFunction: null }, inputTabindex: { classPropertyName: "inputTabindex", publicName: "inputTabindex", isSignal: true, isRequired: false, transformFunction: null }, compact: { classPropertyName: "compact", publicName: "compact", isSignal: true, isRequired: false, transformFunction: null }, colorPicker: { classPropertyName: "colorPicker", publicName: "colorPicker", isSignal: true, isRequired: false, transformFunction: null }, optionTpl: { classPropertyName: "optionTpl", publicName: "optionTpl", isSignal: true, isRequired: false, transformFunction: null }, valueTpl: { classPropertyName: "valueTpl", publicName: "valueTpl", isSignal: true, isRequired: false, transformFunction: null }, panelHeaderTpl: { classPropertyName: "panelHeaderTpl", publicName: "panelHeaderTpl", isSignal: true, isRequired: false, transformFunction: null }, panelFooterTpl: { classPropertyName: "panelFooterTpl", publicName: "panelFooterTpl", isSignal: true, isRequired: false, transformFunction: null }, dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { panelClosed: "panelClosed", panelOpened: "panelOpened", highlightedOption: "highlightedOption", optionTpl: "optionTplChange", valueTpl: "valueTplChange", panelHeaderTpl: "panelHeaderTplChange", panelFooterTpl: "panelFooterTplChange", clueChange: "clueChange", nextPage: "nextPage", addOption: "addOption", dataSource: "dataSourceChange" }, host: { listeners: { "click": "onClickOpenPanel($event)", "keydown": "onKeyDownNavigation($event)" }, properties: { "class.colorPicker": "colorPicker()", "class.mod-compact": "compact()", "class.is-clearable": "isClearable()", "class.is-selected": "isSelectedClass", "class.is-searchFilled": "isSearchFilledClass", "class.mod-noClueIcon": "isNoClueIconClass", "class": "widthClass()" } }, viewQueries: [{ propertyName: "inputElementRef", first: true, predicate: ["inputElement"], descendants: true, isSignal: true }], ngImport: i0 }); }
1074
1214
  }
1075
1215
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: ALuSelectInputComponent, decorators: [{
1076
1216
  type: Directive,
@@ -1082,11 +1222,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
1082
1222
  '[class.is-selected]': 'isSelectedClass',
1083
1223
  '[class.is-searchFilled]': 'isSearchFilledClass',
1084
1224
  '[class.mod-noClueIcon]': 'isNoClueIconClass',
1225
+ '[class]': 'widthClass()',
1085
1226
  '(click)': 'onClickOpenPanel($event)',
1086
1227
  '(keydown)': 'onKeyDownNavigation($event)',
1087
1228
  },
1088
1229
  }]
1089
- }], ctorParameters: () => [], propDecorators: { ignorePresentation: [{ type: i0.Input, args: [{ isSignal: true, alias: "ignorePresentation", required: false }] }], panelClosed: [{ type: i0.Output, args: ["panelClosed"] }], panelOpened: [{ type: i0.Output, args: ["panelOpened"] }], highlightedOption: [{ type: i0.Output, args: ["highlightedOption"] }], inputElementRef: [{ type: i0.ViewChild, args: ['inputElement', { isSignal: true }] }], prefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefix", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], clearableInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], addOptionLabelInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "addOptionLabel", required: false }] }], addOptionStrategy: [{ type: i0.Input, args: [{ isSignal: true, alias: "addOptionStrategy", required: false }] }], overlayConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "overlayConfig", required: false }] }], loadingInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], optionsInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionComparerInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionComparer", required: false }] }], optionKeyInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionKey", required: false }] }], noClueIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "noClueIcon", required: false }] }], inputTabindex: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputTabindex", required: false }] }], compact: [{ type: i0.Input, args: [{ isSignal: true, alias: "compact", required: false }] }], colorPicker: [{ type: i0.Input, args: [{ isSignal: true, alias: "colorPicker", required: false }] }], optionTpl: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionTpl", required: false }] }, { type: i0.Output, args: ["optionTplChange"] }], valueTpl: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueTpl", required: false }] }, { type: i0.Output, args: ["valueTplChange"] }], panelHeaderTpl: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelHeaderTpl", required: false }] }, { type: i0.Output, args: ["panelHeaderTplChange"] }], panelFooterTpl: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelFooterTpl", required: false }] }, { type: i0.Output, args: ["panelFooterTplChange"] }], clueChange: [{ type: i0.Output, args: ["clueChange"] }], nextPage: [{ type: i0.Output, args: ["nextPage"] }], addOption: [{ type: i0.Output, args: ["addOption"] }], dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }, { type: i0.Output, args: ["dataSourceChange"] }] } });
1230
+ }], ctorParameters: () => [], propDecorators: { ignorePresentation: [{ type: i0.Input, args: [{ isSignal: true, alias: "ignorePresentation", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], panelClosed: [{ type: i0.Output, args: ["panelClosed"] }], panelOpened: [{ type: i0.Output, args: ["panelOpened"] }], highlightedOption: [{ type: i0.Output, args: ["highlightedOption"] }], inputElementRef: [{ type: i0.ViewChild, args: ['inputElement', { isSignal: true }] }], prefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefix", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], clearableInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], addOptionLabelInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "addOptionLabel", required: false }] }], addOptionStrategy: [{ type: i0.Input, args: [{ isSignal: true, alias: "addOptionStrategy", required: false }] }], overlayConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "overlayConfig", required: false }] }], loadingInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], optionsInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionComparerInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionComparer", required: false }] }], optionKeyInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionKey", required: false }] }], noClueIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "noClueIcon", required: false }] }], inputTabindex: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputTabindex", required: false }] }], compact: [{ type: i0.Input, args: [{ isSignal: true, alias: "compact", required: false }] }], colorPicker: [{ type: i0.Input, args: [{ isSignal: true, alias: "colorPicker", required: false }] }], optionTpl: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionTpl", required: false }] }, { type: i0.Output, args: ["optionTplChange"] }], valueTpl: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueTpl", required: false }] }, { type: i0.Output, args: ["valueTplChange"] }], panelHeaderTpl: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelHeaderTpl", required: false }] }, { type: i0.Output, args: ["panelHeaderTplChange"] }], panelFooterTpl: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelFooterTpl", required: false }] }, { type: i0.Output, args: ["panelFooterTplChange"] }], clueChange: [{ type: i0.Output, args: ["clueChange"] }], nextPage: [{ type: i0.Output, args: ["nextPage"] }], addOption: [{ type: i0.Output, args: ["addOption"] }], dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }, { type: i0.Output, args: ["dataSourceChange"] }] } });
1090
1231
 
1091
1232
  let selectId = 0;
1092
1233
  function selectIdFactory() {
@@ -1252,6 +1393,93 @@ class LuSelectPanelRef {
1252
1393
  }
1253
1394
  }
1254
1395
 
1396
+ /**
1397
+ * Opens a select panel as a bottom sheet.
1398
+ *
1399
+ * Below the `S` breakpoint the panel stops being a popover anchored to the field and becomes a modal
1400
+ * surface pinned to the bottom of the viewport. That surface already exists as the dialog's `sheet`
1401
+ * mode — backdrop, blocked page scroll, focus trap, focus restoration and close button all come with
1402
+ * it — so the panel is opened as a dialog instead of as a bespoke overlay.
1403
+ *
1404
+ * The opening animation is disabled (`mod-select`, styled by the select packages themselves — see their
1405
+ * `sheet` mixin) and the search input is focused by hand instead of through CDK's `autoFocus`. iOS
1406
+ * Safari only raises the virtual keyboard for a `focus()` call made synchronously inside the tap that
1407
+ * triggered it; CDK's own autofocus defers the actual `focus()` through `afterNextRender`, which always
1408
+ * lands after that window has closed, so the keyboard silently never opens. Forcing a render with
1409
+ * `ApplicationRef.tick()` right here — still inside the same synchronous call stack as the tap — makes
1410
+ * the search input exist in the DOM in time to focus it for real, and skipping the animation keeps the
1411
+ * sheet from visibly jumping once the keyboard's own viewport resize kicks in immediately after.
1412
+ *
1413
+ * @param dialogService the dialog service provided alongside the select input
1414
+ * @param panel the panel component to render inside the sheet
1415
+ * @param injector the injector carrying the panel ref and the select input for the panel
1416
+ * @param ariaLabel names the sheet, so it stays named even before its header has rendered
1417
+ */
1418
+ function openSelectPanelSheet(dialogService, panel, injector, ariaLabel) {
1419
+ const config = {
1420
+ content: panel,
1421
+ mode: 'sheet',
1422
+ size: 'maxContent',
1423
+ ariaLabel: ariaLabel || undefined,
1424
+ // `mod-select` lets the select packages scope their own dialog header tweaks (centered title,
1425
+ // close button pulled out of flow, no opening animation) without reaching into the generic
1426
+ // dialog styles.
1427
+ panelClasses: ['mod-select'],
1428
+ // The panel ref and the select input reach the panel through this injector: the dialog's own
1429
+ // `providers` hook is reserved by the service for `LuDialogRef`. `autoFocus: false` disables
1430
+ // CDK's own deferred autofocus — the search input is focused by hand right below instead.
1431
+ cdkConfigOverride: { injector, autoFocus: false },
1432
+ };
1433
+ const dialogRef = dialogService.open(config);
1434
+ injector.get(ApplicationRef).tick();
1435
+ const searchInput = dialogRef.cdkRef.overlayRef.overlayElement.querySelector('.textField-input-value');
1436
+ searchInput?.focus();
1437
+ trackVisibleViewport(dialogRef);
1438
+ return dialogRef;
1439
+ }
1440
+ /**
1441
+ * Keeps `--components-dialog-visibleViewportBlockSize` and `--components-dialog-insetBlockEnd`
1442
+ * in sync with the visual viewport for as long as the sheet stays open.
1443
+ *
1444
+ * iOS doesn't shrink the layout viewport when the on-screen keyboard opens — only the visual one — so
1445
+ * a sheet pinned with a plain `bottom: 0` stays anchored to the bottom of the full, keyboard-unaware
1446
+ * layout viewport, which is now hidden behind the keyboard: most of the sheet renders underneath it,
1447
+ * clamped height or not. Nudging `bottom` up by the amount the keyboard currently obscures — on top of
1448
+ * clamping the max height — keeps the sheet entirely above the keyboard, using all the space that's
1449
+ * actually visible, instead of a second, outer page scroll on top of the listbox's own.
1450
+ *
1451
+ * Set on the sheet's own overlay element rather than `document.documentElement`: these custom properties
1452
+ * are also read by any other dialog in `mode: 'sheet'` (e.g. approbation-inbox's), and a document-wide
1453
+ * value would leak this select's keyboard tracking onto an unrelated sheet stacked underneath it.
1454
+ */
1455
+ function trackVisibleViewport(dialogRef) {
1456
+ const viewport = window.visualViewport;
1457
+ if (!viewport) {
1458
+ return;
1459
+ }
1460
+ const style = dialogRef.cdkRef.overlayRef.overlayElement.style;
1461
+ const update = () => {
1462
+ style.setProperty('--components-dialog-visibleViewportBlockSize', `${viewport.height}px`);
1463
+ style.setProperty('--components-dialog-insetBlockEnd', `${window.innerHeight - viewport.height - viewport.offsetTop}px`);
1464
+ // The initially-selected option scrolls into view as soon as it's rendered (see
1465
+ // `scrollIntoViewOnceReady`), which — with the opening animation disabled — happens before the
1466
+ // keyboard has finished opening and before the resize above has clamped the sheet down to its
1467
+ // final size. That earlier scroll position is stale once the sheet's own geometry has changed
1468
+ // size underneath it, so it's redone here on every viewport update, landing correctly once the
1469
+ // keyboard settles.
1470
+ dialogRef.cdkRef.overlayRef.overlayElement.querySelector('.is-highlighted')?.scrollIntoView({ block: 'nearest' });
1471
+ };
1472
+ update();
1473
+ viewport.addEventListener('resize', update);
1474
+ viewport.addEventListener('scroll', update);
1475
+ dialogRef.closed$.pipe(take(1)).subscribe(() => {
1476
+ viewport.removeEventListener('resize', update);
1477
+ viewport.removeEventListener('scroll', update);
1478
+ style.removeProperty('--components-dialog-visibleViewportBlockSize');
1479
+ style.removeProperty('--components-dialog-insetBlockEnd');
1480
+ });
1481
+ }
1482
+
1255
1483
  /**
1256
1484
  * Tracks whether the user is currently navigating with the pointer or with the keyboard.
1257
1485
  *
@@ -1353,5 +1581,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
1353
1581
  * Generated bundle index. Do not edit.
1354
1582
  */
1355
1583
 
1356
- export { ALuSelectInputComponent, CORE_SELECT_API_TOTAL_COUNT_PROVIDER, CoreSelectKeyManager, LU_CORE_SELECT_TRANSLATIONS, LU_OPTION_CONTEXT, LuCoreSelectNoClueDirective, LuCoreSelectPanelHeaderDirective, LuCoreSelectTotalCountDirective, LuDisabledOptionDirective, LuDisplayerDirective, LuIsOptionSelectedPipe, LuOptionDirective, LuOptionGroupDirective, LuSelectPanelLayoutComponent, LuSelectPanelRef, LuSimpleSelectDefaultOptionComponent, SELECT_ID, SELECT_LABEL, SELECT_LABEL_ID, SELECT_PANEL_INSTANCE, TreeDisplayPipe, addAttributesOnCdkContainer, applySearchDelimiter, coreSelectDefaultOptionComparer, coreSelectDefaultOptionKey, luCoreSelectTranslations, provideLuSelectLabelsAndIds, provideOptionContext, CoreSelectPanelElement as ɵCoreSelectPanelElement, ɵIsSelectedStrategy, LuOptionComponent as ɵLuOptionComponent, LuOptionGroupPipe as ɵLuOptionGroupPipe, LuOptionOutletDirective as ɵLuOptionOutletDirective, generateGroups as ɵgenerateGroups, getGroupTemplateLocation as ɵgetGroupTemplateLocation, injectPointerNavigation as ɵinjectPointerNavigation, scrollIntoViewOnceReady as ɵscrollIntoViewOnceReady };
1584
+ export { ALuSelectInputComponent, CORE_SELECT_API_TOTAL_COUNT_PROVIDER, CoreSelectKeyManager, LU_CORE_SELECT_TRANSLATIONS, LU_OPTION_CONTEXT, LuCoreSelectNoClueDirective, LuCoreSelectPanelHeaderDirective, LuCoreSelectTotalCountDirective, LuDisabledOptionDirective, LuDisplayerDirective, LuIsOptionSelectedPipe, LuOptionDirective, LuOptionGroupDirective, LuSelectPanelLayoutComponent, LuSelectPanelRef, LuSimpleSelectDefaultOptionComponent, SELECT_ID, SELECT_LABEL, SELECT_LABEL_ID, SELECT_PANEL_INSTANCE, TreeDisplayPipe, addAttributesOnCdkContainer, applySearchDelimiter, coreSelectDefaultOptionComparer, coreSelectDefaultOptionKey, luCoreSelectTranslations, provideLuSelectLabelsAndIds, provideOptionContext, CoreSelectPanelElement as ɵCoreSelectPanelElement, ɵIsSelectedStrategy, LuOptionComponent as ɵLuOptionComponent, LuOptionGroupPipe as ɵLuOptionGroupPipe, LuOptionOutletDirective as ɵLuOptionOutletDirective, fillScrollViewport as ɵfillScrollViewport, generateGroups as ɵgenerateGroups, getGroupTemplateLocation as ɵgetGroupTemplateLocation, injectPointerNavigation as ɵinjectPointerNavigation, openSelectPanelSheet as ɵopenSelectPanelSheet, scrollIntoViewOnceReady as ɵscrollIntoViewOnceReady };
1357
1585
  //# sourceMappingURL=lucca-front-ng-core-select.mjs.map