mn-angular-lib 1.0.168 → 1.0.169
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.
|
@@ -3912,6 +3912,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3912
3912
|
], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'block' }, template: "<!-- Date selector bar. Layout is driven by the bar's own measured width, so it\n rearranges for the space the bar was given, not for the viewport. The strip\n never stacks: as room runs out it shows fewer days, then hands over to the\n picker. Nothing here is boxed \u2014 spacing does the grouping, not borders. -->\n<div class=\"flex flex-col gap-2 px-4 py-3\">\n\n <!-- Month header. Titles the whole bar so the day numbers below are never loose:\n it names the month the strip is in (or, when compact, the month the\n selection sits in), reading as a range where the strip crosses a boundary.\n On its own line it has room to be spelled out, and no longer steals width\n from the days the way the old inline caption did. -->\n <div aria-hidden=\"true\"\n class=\"text-center text-sm font-semibold tracking-wide whitespace-nowrap opacity-80\">\n {{ monthCaption() }}\n </div>\n\n <!-- Controls row. Today and the strip travel together as one centred cluster, so\n Today sits right beside the days instead of stranded against the far edge;\n the gap between them matches the strip's own spacing. When the strip gives\n way to the picker, the picker fills the row and Today anchors its left. -->\n <div class=\"flex items-center justify-center gap-3\">\n\n <!-- Today: returns the strip and the selection to the current day. Outlined\n rather than filled, so the one filled-primary element on the bar is the\n selected day \u2014 Today shares the primary accent without competing with it. -->\n <button (click)=\"goToToday()\"\n [data]=\"{ variant: 'outline', color: 'primary', size: 'md' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n {{ todayLabel() }}\n </button>\n\n <!-- Too narrow for a usable strip, so the picker takes its place beside Today.\n Every date stays reachable on a phone, in one tap rather than by paging;\n the month header above keeps the compact bar labelled like the wide one. -->\n @if (!showDayStrip()) {\n <mn-lib-datetime (ngModelChange)=\"onDateModelChanged($event)\"\n [ngModel]=\"selectedDateString()\"\n [props]=\"pickerProps()\"\n class=\"block min-w-0 flex-1\"></mn-lib-datetime>\n }\n\n <!-- Arrows and days, sized to their own content so the row can centre them\n together with Today as a single cluster \u2014 rather than the days drifting to\n the middle of the leftover space, away from the button. -->\n @if (showDayStrip()) {\n <div [attr.aria-label]=\"dayStripLabel()\"\n class=\"flex min-w-0 items-center gap-3\"\n role=\"group\">\n <button (click)=\"navigatePrevious()\"\n [attr.aria-label]=\"previousLabel()\"\n [data]=\"{ variant: 'text', size: 'md', color: 'gray' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n <svg [size]=\"18\" lucideChevronLeft></svg>\n </button>\n\n <!-- Days read on one line so each sits at the same height as the buttons\n either side of it. Roving tabindex: one day is in the tab order, the\n arrow keys move between the rest and page the strip at either end. -->\n <div class=\"flex min-w-0 gap-1\">\n @for (tile of dayTiles(); track tile.date.getTime(); let i = $index) {\n <!-- Selected is the filled pill; today is called out by its number\n alone, tinted primary \u2014 no box, in keeping with a bar that groups\n by spacing rather than borders. When today is also the selected\n day the fill takes over, since there is nothing left to tell apart.\n A month break is marked by extra breathing room rather than a rule,\n so the run of days stays unbroken; the header names both months. -->\n <button #tileButton\n (click)=\"selectDate(tile.date)\"\n (focus)=\"onTileFocus(i)\"\n (keydown)=\"onTileKeydown($event, i)\"\n [attr.aria-current]=\"tile.isToday ? 'date' : null\"\n [attr.aria-label]=\"tile.accessibleLabel\"\n [attr.aria-pressed]=\"tile.isSelected\"\n [attr.tabindex]=\"isTabbable(i) ? 0 : -1\"\n [class.bg-primary]=\"tile.isSelected\"\n [class.hover:bg-base-200]=\"!tile.isSelected\"\n [class.ml-3]=\"tile.startsNewMonth\"\n [class.text-primary-content]=\"tile.isSelected\"\n class=\"flex min-w-16 shrink-0 cursor-pointer items-center justify-center gap-1.5 rounded-lg px-3 py-2 text-sm transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary motion-reduce:transition-none\"\n type=\"button\">\n <span [class.opacity-60]=\"!tile.isSelected\"\n [class.opacity-90]=\"tile.isSelected\"\n aria-hidden=\"true\"\n class=\"text-xs font-medium uppercase\">\n {{ tile.dayName }}\n </span>\n <!-- Today's number carries the primary tint, unless the tile is\n filled \u2014 then it wears the fill's own text colour instead. -->\n <span [class.text-primary]=\"tile.isToday && !tile.isSelected\"\n aria-hidden=\"true\"\n class=\"font-bold tabular-nums\">{{ tile.dayNumber }}</span>\n </button>\n }\n </div>\n\n <button (click)=\"navigateNext()\"\n [attr.aria-label]=\"nextLabel()\"\n [data]=\"{ variant: 'text', size: 'md', color: 'gray' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n <svg [size]=\"18\" lucideChevronRight></svg>\n </button>\n </div>\n }\n </div>\n</div>\n" }]
|
|
3913
3913
|
}], propDecorators: { selectedDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedDate", required: false }] }], todayLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "todayLabel", required: false }] }], pickDateLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pickDateLabel", required: false }] }], previousLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "previousLabel", required: false }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], dayStripLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "dayStripLabel", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], dateSelected: [{ type: i0.Output, args: ["dateSelected"] }], tileButtons: [{ type: i0.ViewChildren, args: ['tileButton', { isSignal: true }] }] } });
|
|
3914
3914
|
|
|
3915
|
+
/** Breathing room kept between the panel and the viewport edge, in pixels. */
|
|
3916
|
+
const PANEL_VIEWPORT_MARGIN_PX = 8;
|
|
3917
|
+
/**
|
|
3918
|
+
* The least room below the trigger before the panel is flipped above it, in pixels: roughly
|
|
3919
|
+
* four rows, so a picker that still fits its usual few options never flips for no reason.
|
|
3920
|
+
*/
|
|
3921
|
+
const PANEL_FLIP_THRESHOLD_PX = 160;
|
|
3922
|
+
/**
|
|
3923
|
+
* Chooses the side of the trigger with room for the panel.
|
|
3924
|
+
*
|
|
3925
|
+
* Below by default. Above only when the space below is under {@link PANEL_FLIP_THRESHOLD_PX}
|
|
3926
|
+
* and the space above is larger: flipping is the exception, because a list that opens upward
|
|
3927
|
+
* reads backwards from what the trigger's chevron promised.
|
|
3928
|
+
* @param trigger The trigger's viewport rectangle.
|
|
3929
|
+
* @param viewportHeight The viewport's inner height.
|
|
3930
|
+
* @param gap Distance between trigger and panel, in pixels.
|
|
3931
|
+
* @param panelMaxHeight The panel's own height cap in pixels, so the available room only
|
|
3932
|
+
* becomes the cap when it is the smaller of the two.
|
|
3933
|
+
* @returns The styles to bind on the panel.
|
|
3934
|
+
*/
|
|
3935
|
+
function anchoredPanelPlacement(trigger, viewportHeight, gap, panelMaxHeight) {
|
|
3936
|
+
const below = viewportHeight - trigger.bottom - gap - PANEL_VIEWPORT_MARGIN_PX;
|
|
3937
|
+
const above = trigger.top - gap - PANEL_VIEWPORT_MARGIN_PX;
|
|
3938
|
+
const openAbove = below < PANEL_FLIP_THRESHOLD_PX && above > below;
|
|
3939
|
+
const room = Math.max(openAbove ? above : below, 0);
|
|
3940
|
+
return {
|
|
3941
|
+
top: openAbove ? 'auto' : `${trigger.bottom + gap}px`,
|
|
3942
|
+
bottom: openAbove ? `${viewportHeight - trigger.top + gap}px` : 'auto',
|
|
3943
|
+
maxHeight: room < panelMaxHeight ? `${room}px` : null,
|
|
3944
|
+
};
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3915
3947
|
const mnMultiSelectVariants = tv({
|
|
3916
3948
|
base: 'bg-base-100 border-1 border-base-300 text-base-content text-sm cursor-pointer',
|
|
3917
3949
|
variants: {
|
|
@@ -4300,6 +4332,8 @@ class MnMultiSelect {
|
|
|
4300
4332
|
/** Layout classes for the anchored popover panel. The mobile sheet is rendered by
|
|
4301
4333
|
* mn-bottom-sheet instead, so it no longer needs a branch here. */
|
|
4302
4334
|
panelClasses = 'fixed z-9999 bg-base-100 border border-base-300 rounded-md shadow-lg max-h-60 overflow-auto';
|
|
4335
|
+
/** The panel's own height cap in pixels: the `max-h-60` above, restated for the placement maths. */
|
|
4336
|
+
static PANEL_MAX_HEIGHT_PX = 240;
|
|
4303
4337
|
/** Layout classes for the invisible click shield rendered under the anchored panel.
|
|
4304
4338
|
* One step below the panel's z-index so the panel itself stays clickable, and above
|
|
4305
4339
|
* any modal/drawer chrome (which tops out well under 9998). */
|
|
@@ -4366,7 +4400,14 @@ class MnMultiSelect {
|
|
|
4366
4400
|
isDisabled = false;
|
|
4367
4401
|
searchTerm = '';
|
|
4368
4402
|
/** Dropdown position calculated from trigger bounding rect */
|
|
4369
|
-
|
|
4403
|
+
/** Inline placement of the anchored panel; `maxHeight` only binds when the viewport is the tighter cap. */
|
|
4404
|
+
dropdownStyle = {
|
|
4405
|
+
top: '0px',
|
|
4406
|
+
bottom: 'auto',
|
|
4407
|
+
left: '0px',
|
|
4408
|
+
width: '0px',
|
|
4409
|
+
maxHeight: null,
|
|
4410
|
+
};
|
|
4370
4411
|
onChange = () => {
|
|
4371
4412
|
};
|
|
4372
4413
|
onTouched = () => { };
|
|
@@ -4596,13 +4637,16 @@ class MnMultiSelect {
|
|
|
4596
4637
|
}
|
|
4597
4638
|
this.previousBodyOverflow = null;
|
|
4598
4639
|
}
|
|
4599
|
-
/**
|
|
4640
|
+
/**
|
|
4641
|
+
* Calculates the fixed position for the dropdown based on the trigger element: below it
|
|
4642
|
+
* while the viewport has room, above it otherwise, never past the viewport's edge.
|
|
4643
|
+
*/
|
|
4600
4644
|
updateDropdownPosition() {
|
|
4601
4645
|
if (!this.triggerRef)
|
|
4602
4646
|
return;
|
|
4603
4647
|
const rect = this.triggerRef.nativeElement.getBoundingClientRect();
|
|
4604
4648
|
this.dropdownStyle = {
|
|
4605
|
-
|
|
4649
|
+
...anchoredPanelPlacement(rect, window.innerHeight, 0, MnMultiSelect.PANEL_MAX_HEIGHT_PX),
|
|
4606
4650
|
left: `${rect.left}px`,
|
|
4607
4651
|
width: `${rect.width}px`,
|
|
4608
4652
|
};
|
|
@@ -4869,11 +4913,11 @@ class MnMultiSelect {
|
|
|
4869
4913
|
});
|
|
4870
4914
|
}
|
|
4871
4915
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnMultiSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4872
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnMultiSelect, isStandalone: true, selector: "mn-lib-multi-select", inputs: { props: "props" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()", "window:scroll": "onWindowScrollOrResize()", "window:resize": "onWindowScrollOrResize()" } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true }, { propertyName: "dropdownRef", first: true, predicate: ["dropdown"], descendants: true }, { propertyName: "shieldRef", first: true, predicate: ["shield"], descendants: true }, { propertyName: "sheetRef", first: true, predicate: ["sheet"], descendants: true, read: ElementRef }], ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Trigger -->\n <div\n #trigger\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n class=\"relative\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-listbox' : null\"\n role=\"combobox\"\n tabindex=\"0\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n (blur)=\"handleBlur()\"\n >\n <!-- `pr-6` reserves the gutter the caret is absolutely positioned in (right-2 +\n w-4), so a value can never render underneath it. `min-w-0` lets the chips\n shrink below their content width, which is what makes truncation possible. -->\n <div class=\"flex flex-row items-center gap-x-2 flex-wrap min-h-6 min-w-0 pr-6\">\n @if (selectedOptions.length === 0) {\n <span class=\"text-base-content/50\">{{ placeholderLabel }}</span>\n } @else if (isCollapsed) {\n <span\n class=\"inline-flex items-center max-w-full truncate bg-base-200 border border-accent text-base-content text-xs px-2 py-0.5 rounded-md\">\n {{ collapseSummaryText }}\n </span>\n } @else {\n @for (opt of selectedOptions; track opt.value) {\n <!-- Only the \u00D7 removes. The chip body deliberately carries no handler, so a click\n anywhere on it bubbles to the trigger and just opens/closes the panel \u2014 clicking\n the trigger to dismiss the dropdown must never silently delete a selection. -->\n <span\n class=\"inline-flex items-center gap-x-1 max-w-full min-w-0 bg-base-200 border border-accent text-base-content text-xs pl-2 py-0.5 rounded-md\">\n <span [attr.title]=\"opt.label\" class=\"truncate\">{{ opt.label }}</span>\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary', hover: false }\"\n type=\"button\"\n class=\"text-base-content/50 cursor-pointer shrink-0\"\n (click)=\"removeOption(opt, $event)\"\n [attr.aria-label]=\"'Remove ' + opt.label\"\n ><svg lucideX [size]=\"18\"></svg></button>\n </span>\n }\n }\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg [size]=\"16\" class=\"text-base-content/50\" lucideChevronDown></svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the panel is presented as a shared bottom sheet: the sheet chrome\n (backdrop, grabber, swipe/flick-to-dismiss, slide animation) lives in\n mn-bottom-sheet; this component only projects the field's content into it.\n The sheet host is portalled to document.body (see the `sheet` ViewChild) so\n its `position: fixed` anchors to the viewport, not a transformed ancestor. -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"uiConfig.ariaLabel || uiConfig.label || props.label || uiConfig.placeholder || props.placeholder\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div\n [id]=\"resolvedId + '-listbox'\"\n aria-multiselectable=\"true\"\n class=\"flex flex-col flex-1 min-h-0 overflow-hidden\"\n role=\"listbox\"\n >\n <!-- The sheet covers its own trigger, so it carries a header to name the field; the\n way out is the grabber (swipe-to-dismiss) and a backdrop tap. -->\n <div class=\"px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-medium text-base-content truncate\">\n {{ uiConfig.label || props.label || uiConfig.placeholder || props.placeholder || '' }}\n </p>\n </div>\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- A transparent full-viewport shield behind the panel, so \"click anywhere to\n dismiss\" is literally true. It also *consumes* that click: without it the click\n reaches whatever sits underneath \u2014 inside a modal that is the modal's own\n backdrop, so dismissing the dropdown would tear down the whole modal with it.\n Portalled to document.body for the same reason the panel is. It is aria-hidden\n and unfocusable: the keyboard equivalent of this click is Escape. -->\n <div\n #shield\n (click)=\"onShieldClick($event)\"\n [id]=\"resolvedId + '-shield'\"\n [ngClass]=\"shieldClasses\"\n aria-hidden=\"true\"\n ></div>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-listbox'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.width]=\"dropdownStyle.width\"\n aria-multiselectable=\"true\"\n role=\"listbox\"\n >\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n }\n }\n\n <!-- The search box + option list, shared verbatim by the sheet and the anchored\n popover. `isSheet` only tunes spacing/sizing and which element scrolls: in sheet\n mode the list is the flex scroller; anchored, the popover itself scrolls. -->\n <ng-template #panelBody>\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n [ngClass]=\"isSheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: searchPlaceholderLabel,\n ariaLabel: searchPlaceholderLabel,\n fullWidth: true,\n size: 'sm'\n }\"\n ></mn-lib-input-field>\n </div>\n }\n <div [ngClass]=\"isSheet ? 'flex-1 overflow-auto overscroll-contain' : ''\">\n @for (opt of filteredOptions; track opt.value) {\n <div\n (click)=\"toggleOption(opt); $event.stopPropagation()\"\n (keyup.enter)=\"toggleOption(opt)\"\n (keyup.space)=\"toggleOption(opt)\"\n [attr.aria-selected]=\"isSelected(opt)\"\n [class.opacity-50]=\"opt.disabled || isMaxReached(opt)\"\n [class.pointer-events-none]=\"opt.disabled || isMaxReached(opt)\"\n [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\"\n class=\"flex items-center gap-x-2 cursor-pointer text-base-content hover:bg-base-200\"\n role=\"option\"\n tabindex=\"0\"\n >\n <input\n [checked]=\"isSelected(opt)\"\n [disabled]=\"opt.disabled || isMaxReached(opt)\"\n class=\"w-4 h-4 accent-primary pointer-events-none shrink-0\"\n tabindex=\"-1\"\n type=\"checkbox\"\n />\n <span>{{ opt.label }}</span>\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\" class=\"text-base-content/50\">\n {{ noOptionsLabel }}\n </div>\n }\n </div>\n </ng-template>\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n }\n }\n }\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnErrorMessage, selector: "mn-error-message", inputs: ["errorMessage", "id"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnBottomSheet, selector: "mn-bottom-sheet", inputs: ["showBackdrop", "showGrabber", "dismissible", "minHeightPx", "maxHeightVh", "containerClass", "ariaLabel", "ariaLabelledby", "growWithKeyboard", "dismissGuard"], outputs: ["dismiss"] }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }, { kind: "component", type: LucideChevronDown, selector: "svg[lucideChevronDown]" }] });
|
|
4916
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnMultiSelect, isStandalone: true, selector: "mn-lib-multi-select", inputs: { props: "props" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()", "window:scroll": "onWindowScrollOrResize()", "window:resize": "onWindowScrollOrResize()" } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true }, { propertyName: "dropdownRef", first: true, predicate: ["dropdown"], descendants: true }, { propertyName: "shieldRef", first: true, predicate: ["shield"], descendants: true }, { propertyName: "sheetRef", first: true, predicate: ["sheet"], descendants: true, read: ElementRef }], ngImport: i0, template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Trigger -->\n <div\n #trigger\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n class=\"relative\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-listbox' : null\"\n role=\"combobox\"\n tabindex=\"0\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n (blur)=\"handleBlur()\"\n >\n <!-- `pr-6` reserves the gutter the caret is absolutely positioned in (right-2 +\n w-4), so a value can never render underneath it. `min-w-0` lets the chips\n shrink below their content width, which is what makes truncation possible. -->\n <div class=\"flex flex-row items-center gap-x-2 flex-wrap min-h-6 min-w-0 pr-6\">\n @if (selectedOptions.length === 0) {\n <span class=\"text-base-content/50\">{{ placeholderLabel }}</span>\n } @else if (isCollapsed) {\n <span\n class=\"inline-flex items-center max-w-full truncate bg-base-200 border border-accent text-base-content text-xs px-2 py-0.5 rounded-md\">\n {{ collapseSummaryText }}\n </span>\n } @else {\n @for (opt of selectedOptions; track opt.value) {\n <!-- Only the \u00D7 removes. The chip body deliberately carries no handler, so a click\n anywhere on it bubbles to the trigger and just opens/closes the panel \u2014 clicking\n the trigger to dismiss the dropdown must never silently delete a selection. -->\n <span\n class=\"inline-flex items-center gap-x-1 max-w-full min-w-0 bg-base-200 border border-accent text-base-content text-xs pl-2 py-0.5 rounded-md\">\n <span [attr.title]=\"opt.label\" class=\"truncate\">{{ opt.label }}</span>\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary', hover: false }\"\n type=\"button\"\n class=\"text-base-content/50 cursor-pointer shrink-0\"\n (click)=\"removeOption(opt, $event)\"\n [attr.aria-label]=\"'Remove ' + opt.label\"\n ><svg lucideX [size]=\"18\"></svg></button>\n </span>\n }\n }\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg [size]=\"16\" class=\"text-base-content/50\" lucideChevronDown></svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the panel is presented as a shared bottom sheet: the sheet chrome\n (backdrop, grabber, swipe/flick-to-dismiss, slide animation) lives in\n mn-bottom-sheet; this component only projects the field's content into it.\n The sheet host is portalled to document.body (see the `sheet` ViewChild) so\n its `position: fixed` anchors to the viewport, not a transformed ancestor. -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"uiConfig.ariaLabel || uiConfig.label || props.label || uiConfig.placeholder || props.placeholder\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div\n [id]=\"resolvedId + '-listbox'\"\n aria-multiselectable=\"true\"\n class=\"flex flex-col flex-1 min-h-0 overflow-hidden\"\n role=\"listbox\"\n >\n <!-- The sheet covers its own trigger, so it carries a header to name the field; the\n way out is the grabber (swipe-to-dismiss) and a backdrop tap. -->\n <div class=\"px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-medium text-base-content truncate\">\n {{ uiConfig.label || props.label || uiConfig.placeholder || props.placeholder || '' }}\n </p>\n </div>\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- A transparent full-viewport shield behind the panel, so \"click anywhere to\n dismiss\" is literally true. It also *consumes* that click: without it the click\n reaches whatever sits underneath \u2014 inside a modal that is the modal's own\n backdrop, so dismissing the dropdown would tear down the whole modal with it.\n Portalled to document.body for the same reason the panel is. It is aria-hidden\n and unfocusable: the keyboard equivalent of this click is Escape. -->\n <div\n #shield\n (click)=\"onShieldClick($event)\"\n [id]=\"resolvedId + '-shield'\"\n [ngClass]=\"shieldClasses\"\n aria-hidden=\"true\"\n ></div>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-listbox'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.bottom]=\"dropdownStyle.bottom\"\n [style.max-height]=\"dropdownStyle.maxHeight\"\n [style.width]=\"dropdownStyle.width\"\n aria-multiselectable=\"true\"\n role=\"listbox\"\n >\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n }\n }\n\n <!-- The search box + option list, shared verbatim by the sheet and the anchored\n popover. `isSheet` only tunes spacing/sizing and which element scrolls: in sheet\n mode the list is the flex scroller; anchored, the popover itself scrolls. -->\n <ng-template #panelBody>\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n [ngClass]=\"isSheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: searchPlaceholderLabel,\n ariaLabel: searchPlaceholderLabel,\n fullWidth: true,\n size: 'sm'\n }\"\n ></mn-lib-input-field>\n </div>\n }\n <div [ngClass]=\"isSheet ? 'flex-1 overflow-auto overscroll-contain' : ''\">\n @for (opt of filteredOptions; track opt.value) {\n <div\n (click)=\"toggleOption(opt); $event.stopPropagation()\"\n (keyup.enter)=\"toggleOption(opt)\"\n (keyup.space)=\"toggleOption(opt)\"\n [attr.aria-selected]=\"isSelected(opt)\"\n [class.opacity-50]=\"opt.disabled || isMaxReached(opt)\"\n [class.pointer-events-none]=\"opt.disabled || isMaxReached(opt)\"\n [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\"\n class=\"flex items-center gap-x-2 cursor-pointer text-base-content hover:bg-base-200\"\n role=\"option\"\n tabindex=\"0\"\n >\n <input\n [checked]=\"isSelected(opt)\"\n [disabled]=\"opt.disabled || isMaxReached(opt)\"\n class=\"w-4 h-4 accent-primary pointer-events-none shrink-0\"\n tabindex=\"-1\"\n type=\"checkbox\"\n />\n <span>{{ opt.label }}</span>\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\" class=\"text-base-content/50\">\n {{ noOptionsLabel }}\n </div>\n }\n </div>\n </ng-template>\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n }\n }\n }\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnErrorMessage, selector: "mn-error-message", inputs: ["errorMessage", "id"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnBottomSheet, selector: "mn-bottom-sheet", inputs: ["showBackdrop", "showGrabber", "dismissible", "minHeightPx", "maxHeightVh", "containerClass", "ariaLabel", "ariaLabelledby", "growWithKeyboard", "dismissGuard"], outputs: ["dismiss"] }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }, { kind: "component", type: LucideChevronDown, selector: "svg[lucideChevronDown]" }] });
|
|
4873
4917
|
}
|
|
4874
4918
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnMultiSelect, decorators: [{
|
|
4875
4919
|
type: Component,
|
|
4876
|
-
args: [{ selector: 'mn-lib-multi-select', standalone: true, imports: [NgClass, NgTemplateOutlet, FormsModule, MnErrorMessage, MnButton, MnInputField, MnBottomSheet, LucideX, LucideChevronDown], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Trigger -->\n <div\n #trigger\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n class=\"relative\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-listbox' : null\"\n role=\"combobox\"\n tabindex=\"0\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n (blur)=\"handleBlur()\"\n >\n <!-- `pr-6` reserves the gutter the caret is absolutely positioned in (right-2 +\n w-4), so a value can never render underneath it. `min-w-0` lets the chips\n shrink below their content width, which is what makes truncation possible. -->\n <div class=\"flex flex-row items-center gap-x-2 flex-wrap min-h-6 min-w-0 pr-6\">\n @if (selectedOptions.length === 0) {\n <span class=\"text-base-content/50\">{{ placeholderLabel }}</span>\n } @else if (isCollapsed) {\n <span\n class=\"inline-flex items-center max-w-full truncate bg-base-200 border border-accent text-base-content text-xs px-2 py-0.5 rounded-md\">\n {{ collapseSummaryText }}\n </span>\n } @else {\n @for (opt of selectedOptions; track opt.value) {\n <!-- Only the \u00D7 removes. The chip body deliberately carries no handler, so a click\n anywhere on it bubbles to the trigger and just opens/closes the panel \u2014 clicking\n the trigger to dismiss the dropdown must never silently delete a selection. -->\n <span\n class=\"inline-flex items-center gap-x-1 max-w-full min-w-0 bg-base-200 border border-accent text-base-content text-xs pl-2 py-0.5 rounded-md\">\n <span [attr.title]=\"opt.label\" class=\"truncate\">{{ opt.label }}</span>\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary', hover: false }\"\n type=\"button\"\n class=\"text-base-content/50 cursor-pointer shrink-0\"\n (click)=\"removeOption(opt, $event)\"\n [attr.aria-label]=\"'Remove ' + opt.label\"\n ><svg lucideX [size]=\"18\"></svg></button>\n </span>\n }\n }\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg [size]=\"16\" class=\"text-base-content/50\" lucideChevronDown></svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the panel is presented as a shared bottom sheet: the sheet chrome\n (backdrop, grabber, swipe/flick-to-dismiss, slide animation) lives in\n mn-bottom-sheet; this component only projects the field's content into it.\n The sheet host is portalled to document.body (see the `sheet` ViewChild) so\n its `position: fixed` anchors to the viewport, not a transformed ancestor. -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"uiConfig.ariaLabel || uiConfig.label || props.label || uiConfig.placeholder || props.placeholder\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div\n [id]=\"resolvedId + '-listbox'\"\n aria-multiselectable=\"true\"\n class=\"flex flex-col flex-1 min-h-0 overflow-hidden\"\n role=\"listbox\"\n >\n <!-- The sheet covers its own trigger, so it carries a header to name the field; the\n way out is the grabber (swipe-to-dismiss) and a backdrop tap. -->\n <div class=\"px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-medium text-base-content truncate\">\n {{ uiConfig.label || props.label || uiConfig.placeholder || props.placeholder || '' }}\n </p>\n </div>\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- A transparent full-viewport shield behind the panel, so \"click anywhere to\n dismiss\" is literally true. It also *consumes* that click: without it the click\n reaches whatever sits underneath \u2014 inside a modal that is the modal's own\n backdrop, so dismissing the dropdown would tear down the whole modal with it.\n Portalled to document.body for the same reason the panel is. It is aria-hidden\n and unfocusable: the keyboard equivalent of this click is Escape. -->\n <div\n #shield\n (click)=\"onShieldClick($event)\"\n [id]=\"resolvedId + '-shield'\"\n [ngClass]=\"shieldClasses\"\n aria-hidden=\"true\"\n ></div>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-listbox'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.width]=\"dropdownStyle.width\"\n aria-multiselectable=\"true\"\n role=\"listbox\"\n >\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n }\n }\n\n <!-- The search box + option list, shared verbatim by the sheet and the anchored\n popover. `isSheet` only tunes spacing/sizing and which element scrolls: in sheet\n mode the list is the flex scroller; anchored, the popover itself scrolls. -->\n <ng-template #panelBody>\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n [ngClass]=\"isSheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: searchPlaceholderLabel,\n ariaLabel: searchPlaceholderLabel,\n fullWidth: true,\n size: 'sm'\n }\"\n ></mn-lib-input-field>\n </div>\n }\n <div [ngClass]=\"isSheet ? 'flex-1 overflow-auto overscroll-contain' : ''\">\n @for (opt of filteredOptions; track opt.value) {\n <div\n (click)=\"toggleOption(opt); $event.stopPropagation()\"\n (keyup.enter)=\"toggleOption(opt)\"\n (keyup.space)=\"toggleOption(opt)\"\n [attr.aria-selected]=\"isSelected(opt)\"\n [class.opacity-50]=\"opt.disabled || isMaxReached(opt)\"\n [class.pointer-events-none]=\"opt.disabled || isMaxReached(opt)\"\n [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\"\n class=\"flex items-center gap-x-2 cursor-pointer text-base-content hover:bg-base-200\"\n role=\"option\"\n tabindex=\"0\"\n >\n <input\n [checked]=\"isSelected(opt)\"\n [disabled]=\"opt.disabled || isMaxReached(opt)\"\n class=\"w-4 h-4 accent-primary pointer-events-none shrink-0\"\n tabindex=\"-1\"\n type=\"checkbox\"\n />\n <span>{{ opt.label }}</span>\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\" class=\"text-base-content/50\">\n {{ noOptionsLabel }}\n </div>\n }\n </div>\n </ng-template>\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n }\n }\n }\n</div>\n" }]
|
|
4920
|
+
args: [{ selector: 'mn-lib-multi-select', standalone: true, imports: [NgClass, NgTemplateOutlet, FormsModule, MnErrorMessage, MnButton, MnInputField, MnBottomSheet, LucideX, LucideChevronDown], template: "<div class=\"flex flex-col h-full\" [class.is-fullwidth]=\"props.fullWidth\">\n @if (uiConfig.label || props.label) {\n <label class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\" [attr.for]=\"resolvedId\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Trigger -->\n <div\n #trigger\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n class=\"relative\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-listbox' : null\"\n role=\"combobox\"\n tabindex=\"0\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n (blur)=\"handleBlur()\"\n >\n <!-- `pr-6` reserves the gutter the caret is absolutely positioned in (right-2 +\n w-4), so a value can never render underneath it. `min-w-0` lets the chips\n shrink below their content width, which is what makes truncation possible. -->\n <div class=\"flex flex-row items-center gap-x-2 flex-wrap min-h-6 min-w-0 pr-6\">\n @if (selectedOptions.length === 0) {\n <span class=\"text-base-content/50\">{{ placeholderLabel }}</span>\n } @else if (isCollapsed) {\n <span\n class=\"inline-flex items-center max-w-full truncate bg-base-200 border border-accent text-base-content text-xs px-2 py-0.5 rounded-md\">\n {{ collapseSummaryText }}\n </span>\n } @else {\n @for (opt of selectedOptions; track opt.value) {\n <!-- Only the \u00D7 removes. The chip body deliberately carries no handler, so a click\n anywhere on it bubbles to the trigger and just opens/closes the panel \u2014 clicking\n the trigger to dismiss the dropdown must never silently delete a selection. -->\n <span\n class=\"inline-flex items-center gap-x-1 max-w-full min-w-0 bg-base-200 border border-accent text-base-content text-xs pl-2 py-0.5 rounded-md\">\n <span [attr.title]=\"opt.label\" class=\"truncate\">{{ opt.label }}</span>\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary', hover: false }\"\n type=\"button\"\n class=\"text-base-content/50 cursor-pointer shrink-0\"\n (click)=\"removeOption(opt, $event)\"\n [attr.aria-label]=\"'Remove ' + opt.label\"\n ><svg lucideX [size]=\"18\"></svg></button>\n </span>\n }\n }\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg [size]=\"16\" class=\"text-base-content/50\" lucideChevronDown></svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the panel is presented as a shared bottom sheet: the sheet chrome\n (backdrop, grabber, swipe/flick-to-dismiss, slide animation) lives in\n mn-bottom-sheet; this component only projects the field's content into it.\n The sheet host is portalled to document.body (see the `sheet` ViewChild) so\n its `position: fixed` anchors to the viewport, not a transformed ancestor. -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"uiConfig.ariaLabel || uiConfig.label || props.label || uiConfig.placeholder || props.placeholder\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div\n [id]=\"resolvedId + '-listbox'\"\n aria-multiselectable=\"true\"\n class=\"flex flex-col flex-1 min-h-0 overflow-hidden\"\n role=\"listbox\"\n >\n <!-- The sheet covers its own trigger, so it carries a header to name the field; the\n way out is the grabber (swipe-to-dismiss) and a backdrop tap. -->\n <div class=\"px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-medium text-base-content truncate\">\n {{ uiConfig.label || props.label || uiConfig.placeholder || props.placeholder || '' }}\n </p>\n </div>\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- A transparent full-viewport shield behind the panel, so \"click anywhere to\n dismiss\" is literally true. It also *consumes* that click: without it the click\n reaches whatever sits underneath \u2014 inside a modal that is the modal's own\n backdrop, so dismissing the dropdown would tear down the whole modal with it.\n Portalled to document.body for the same reason the panel is. It is aria-hidden\n and unfocusable: the keyboard equivalent of this click is Escape. -->\n <div\n #shield\n (click)=\"onShieldClick($event)\"\n [id]=\"resolvedId + '-shield'\"\n [ngClass]=\"shieldClasses\"\n aria-hidden=\"true\"\n ></div>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-listbox'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.bottom]=\"dropdownStyle.bottom\"\n [style.max-height]=\"dropdownStyle.maxHeight\"\n [style.width]=\"dropdownStyle.width\"\n aria-multiselectable=\"true\"\n role=\"listbox\"\n >\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n }\n }\n\n <!-- The search box + option list, shared verbatim by the sheet and the anchored\n popover. `isSheet` only tunes spacing/sizing and which element scrolls: in sheet\n mode the list is the flex scroller; anchored, the popover itself scrolls. -->\n <ng-template #panelBody>\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n [ngClass]=\"isSheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: searchPlaceholderLabel,\n ariaLabel: searchPlaceholderLabel,\n fullWidth: true,\n size: 'sm'\n }\"\n ></mn-lib-input-field>\n </div>\n }\n <div [ngClass]=\"isSheet ? 'flex-1 overflow-auto overscroll-contain' : ''\">\n @for (opt of filteredOptions; track opt.value) {\n <div\n (click)=\"toggleOption(opt); $event.stopPropagation()\"\n (keyup.enter)=\"toggleOption(opt)\"\n (keyup.space)=\"toggleOption(opt)\"\n [attr.aria-selected]=\"isSelected(opt)\"\n [class.opacity-50]=\"opt.disabled || isMaxReached(opt)\"\n [class.pointer-events-none]=\"opt.disabled || isMaxReached(opt)\"\n [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\"\n class=\"flex items-center gap-x-2 cursor-pointer text-base-content hover:bg-base-200\"\n role=\"option\"\n tabindex=\"0\"\n >\n <input\n [checked]=\"isSelected(opt)\"\n [disabled]=\"opt.disabled || isMaxReached(opt)\"\n class=\"w-4 h-4 accent-primary pointer-events-none shrink-0\"\n tabindex=\"-1\"\n type=\"checkbox\"\n />\n <span>{{ opt.label }}</span>\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\" class=\"text-base-content/50\">\n {{ noOptionsLabel }}\n </div>\n }\n </div>\n </ng-template>\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n }\n }\n }\n</div>\n" }]
|
|
4877
4921
|
}], ctorParameters: () => [], propDecorators: { props: [{
|
|
4878
4922
|
type: Input,
|
|
4879
4923
|
args: [{ required: true }]
|
|
@@ -5047,7 +5091,13 @@ class MnDropdown {
|
|
|
5047
5091
|
* is {@link MnDropdownProps.searchable}. */
|
|
5048
5092
|
searchTerm = '';
|
|
5049
5093
|
/** Popover position computed from the trigger's bounding rect. */
|
|
5050
|
-
|
|
5094
|
+
/** Inline placement of the anchored panel; `maxHeight` only binds when the viewport is the tighter cap. */
|
|
5095
|
+
dropdownStyle = {
|
|
5096
|
+
top: '0px',
|
|
5097
|
+
bottom: 'auto',
|
|
5098
|
+
left: '0px',
|
|
5099
|
+
maxHeight: null,
|
|
5100
|
+
};
|
|
5051
5101
|
/**
|
|
5052
5102
|
* The popover panel, relocated to `document.body` on appearance (see mn-multi-select's
|
|
5053
5103
|
* portal rationale) and detached when the query clears on close/destroy.
|
|
@@ -5208,8 +5258,10 @@ class MnDropdown {
|
|
|
5208
5258
|
const rect = this.triggerRef.nativeElement.getBoundingClientRect();
|
|
5209
5259
|
// The panel is right-aligned to the trigger via a `-translate-x-full` class, so
|
|
5210
5260
|
// `left` is anchored to the trigger's right edge.
|
|
5261
|
+
// Below the trigger while the viewport has room, above it otherwise, never past the edge;
|
|
5262
|
+
// the cap it competes with is the panel's own 60vh.
|
|
5211
5263
|
this.dropdownStyle = {
|
|
5212
|
-
|
|
5264
|
+
...anchoredPanelPlacement(rect, window.innerHeight, 4, window.innerHeight * 0.6),
|
|
5213
5265
|
left: `${rect.right}px`,
|
|
5214
5266
|
};
|
|
5215
5267
|
}
|
|
@@ -5482,11 +5534,11 @@ class MnDropdown {
|
|
|
5482
5534
|
return this.datasource.id ?? this.autoId;
|
|
5483
5535
|
}
|
|
5484
5536
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnDropdown, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5485
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnDropdown, isStandalone: true, selector: "mn-lib-dropdown", inputs: { datasource: "datasource" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()", "window:scroll": "onWindowScrollOrResize()", "window:resize": "onWindowScrollOrResize()" } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, read: ElementRef }, { propertyName: "dropdownRef", first: true, predicate: ["dropdown"], descendants: true }, { propertyName: "sheetRef", first: true, predicate: ["sheet"], descendants: true, read: ElementRef }], ngImport: i0, template: "<div class=\"relative inline-flex\">\n <!-- Trigger -->\n <button\n #trigger\n mnButton\n type=\"button\"\n [id]=\"resolvedId\"\n [data]=\"triggerData\"\n [ngClass]=\"triggerClasses\"\n [attr.aria-label]=\"triggerLabelText ? null : triggerAriaLabel\"\n [attr.aria-haspopup]=\"'menu'\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-menu' : null\"\n (click)=\"toggle()\"\n >\n @if (triggerLabelText) {\n <span class=\"truncate\">{{ triggerLabelText }}</span>\n }\n <!-- One glyph, rendered like a menu item's icon: a caller's template, else lucide data\n (a preset resolves to its own data). No per-preset switch. -->\n @if (triggerIconTemplate; as tpl) {\n <span class=\"shrink-0 inline-flex items-center\">\n <ng-container [ngTemplateOutlet]=\"tpl\"></ng-container>\n </span>\n } @else if (triggerIconData; as glyph) {\n <span class=\"shrink-0 inline-flex items-center\">\n <svg [lucideIcon]=\"glyph.data\" [size]=\"glyph.size\" [class.opacity-70]=\"glyph.dim\"></svg>\n </span>\n }\n </button>\n\n <!-- Menu -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the menu is presented as the shared bottom sheet: chrome (backdrop,\n grabber, swipe-to-dismiss, slide animation) lives in mn-bottom-sheet; this\n component only projects the item list into it. The host is portalled to\n document.body (see the `sheet` ViewChild). -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"menuLabel || triggerAriaLabel\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div [id]=\"resolvedId + '-menu'\" role=\"menu\" class=\"flex flex-col flex-1 min-h-0 overflow-hidden\">\n <!-- The sheet covers its own trigger, so it carries a header to name itself; the\n way out is the grabber (swipe-to-dismiss) and a backdrop tap. -->\n <div class=\"px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-bold text-base-content truncate\">{{ menuLabel || triggerAriaLabel }}</p>\n </div>\n <ng-container [ngTemplateOutlet]=\"searchBox\" [ngTemplateOutletContext]=\"{ sheet: true }\"></ng-container>\n <div class=\"flex-1 flex flex-col overflow-auto overscroll-contain\">\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: true }\"></ng-container>\n </div>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-menu'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.height.px]=\"panelFloorPx\"\n [style.width.px]=\"panelWidthPx\"\n role=\"menu\"\n >\n @if (menuLabel) {\n <p class=\"px-3 pt-1 pb-1.5 text-xs font-medium text-base-content/50 truncate shrink-0\">{{ menuLabel }}</p>\n }\n <ng-container [ngTemplateOutlet]=\"searchBox\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n @if (isSearchable) {\n <!-- Its own scroll region so the pinned search box above stays fixed and the\n locked panel height (panelFloorPx) doesn't change as the list filters. The\n flex column lets the empty state fill and centre in the reserved space. -->\n <div class=\"flex-1 min-h-0 flex flex-col overflow-auto\">\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n }\n </div>\n }\n }\n\n <!-- The item list, shared verbatim by the sheet and the anchored popover. `sheet`\n only tunes the spacing so touch targets are roomier on mobile. -->\n <ng-template #items let-sheet=\"sheet\">\n @for (item of filteredActions; track $index) {\n @if (asAction(item); as action) {\n <button\n type=\"button\"\n role=\"menuitem\"\n [disabled]=\"action.disabled\"\n [attr.aria-current]=\"action.active ? 'true' : null\"\n [class.opacity-50]=\"action.disabled\"\n [class.pointer-events-none]=\"action.disabled\"\n [ngClass]=\"[actionColorClass(action), sheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm', action.active ? 'bg-primary/10 font-medium' : '']\"\n class=\"flex w-full shrink-0 items-center gap-x-2.5 text-left cursor-pointer hover:bg-base-200 focus-visible:bg-base-200 focus:outline-none transition-colors\"\n (click)=\"select(action)\"\n >\n @if (action.icon) {\n <span class=\"shrink-0 inline-flex items-center\">\n @if (isTemplateRef(action.icon)) {\n <ng-container [ngTemplateOutlet]=\"action.icon\"></ng-container>\n } @else {\n <!-- Data icon: rendered here so the item sizes it to match its own text\n (larger in the sheet, where the rows are touch-sized). -->\n <svg [lucideIcon]=\"$any(action.icon)\" [size]=\"sheet ? 18 : 16\"></svg>\n }\n </span>\n }\n <span class=\"truncate min-w-0\">{{ actionLabel(action) }}</span>\n <!-- The current choice's marker (e.g. the active language). Decorative: the\n state is conveyed to assistive tech by `aria-current` on the row. -->\n @if (action.active) {\n <svg\n [lucideIcon]=\"checkIcon\"\n [size]=\"sheet ? 18 : 16\"\n class=\"ml-auto shrink-0 text-primary\"\n aria-hidden=\"true\"\n ></svg>\n }\n </button>\n } @else {\n <hr role=\"separator\" [ngClass]=\"sheet ? 'mx-4 my-1.5' : 'mx-2 my-1'\" class=\"shrink-0 border-t border-base-300\" />\n }\n }\n @if (isSearchable && filteredActions.length === 0) {\n <div class=\"flex-1 flex flex-col items-center justify-center gap-2 px-4 py-6 text-center text-base-content/50\">\n <svg lucideSearchX [size]=\"sheet ? 28 : 24\" class=\"opacity-60\"></svg>\n <span [ngClass]=\"sheet ? 'text-base' : 'text-sm'\">{{ searchEmptyLabel }}</span>\n </div>\n }\n </ng-template>\n\n <!-- The filter input, shared by the sheet and the anchored popover. Autofocused on\n desktop (`!sheet`) so typing starts immediately; deliberately not on mobile, where\n it would pop the soft keyboard and fight the viewport-anchored sheet. Enter runs\n the first visible action via the wrapper's bubbled keydown. -->\n <ng-template #searchBox let-sheet=\"sheet\">\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n (keydown.enter)=\"selectFirstVisible()\"\n [ngClass]=\"sheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: searchPlaceholder,\n ariaLabel: searchPlaceholder,\n fullWidth: true,\n size: 'sm',\n autoFocus: !sheet\n }\"\n ></mn-lib-input-field>\n </div>\n }\n </ng-template>\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnBottomSheet, selector: "mn-bottom-sheet", inputs: ["showBackdrop", "showGrabber", "dismissible", "minHeightPx", "maxHeightVh", "containerClass", "ariaLabel", "ariaLabelledby", "growWithKeyboard", "dismissGuard"], outputs: ["dismiss"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: LucideSearchX, selector: "svg[lucideSearchX]" }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }] });
|
|
5537
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnDropdown, isStandalone: true, selector: "mn-lib-dropdown", inputs: { datasource: "datasource" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()", "window:scroll": "onWindowScrollOrResize()", "window:resize": "onWindowScrollOrResize()" } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, read: ElementRef }, { propertyName: "dropdownRef", first: true, predicate: ["dropdown"], descendants: true }, { propertyName: "sheetRef", first: true, predicate: ["sheet"], descendants: true, read: ElementRef }], ngImport: i0, template: "<div class=\"relative inline-flex\">\n <!-- Trigger -->\n <button\n #trigger\n mnButton\n type=\"button\"\n [id]=\"resolvedId\"\n [data]=\"triggerData\"\n [ngClass]=\"triggerClasses\"\n [attr.aria-label]=\"triggerLabelText ? null : triggerAriaLabel\"\n [attr.aria-haspopup]=\"'menu'\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-menu' : null\"\n (click)=\"toggle()\"\n >\n @if (triggerLabelText) {\n <span class=\"truncate\">{{ triggerLabelText }}</span>\n }\n <!-- One glyph, rendered like a menu item's icon: a caller's template, else lucide data\n (a preset resolves to its own data). No per-preset switch. -->\n @if (triggerIconTemplate; as tpl) {\n <span class=\"shrink-0 inline-flex items-center\">\n <ng-container [ngTemplateOutlet]=\"tpl\"></ng-container>\n </span>\n } @else if (triggerIconData; as glyph) {\n <span class=\"shrink-0 inline-flex items-center\">\n <svg [lucideIcon]=\"glyph.data\" [size]=\"glyph.size\" [class.opacity-70]=\"glyph.dim\"></svg>\n </span>\n }\n </button>\n\n <!-- Menu -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the menu is presented as the shared bottom sheet: chrome (backdrop,\n grabber, swipe-to-dismiss, slide animation) lives in mn-bottom-sheet; this\n component only projects the item list into it. The host is portalled to\n document.body (see the `sheet` ViewChild). -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"menuLabel || triggerAriaLabel\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div [id]=\"resolvedId + '-menu'\" role=\"menu\" class=\"flex flex-col flex-1 min-h-0 overflow-hidden\">\n <!-- The sheet covers its own trigger, so it carries a header to name itself; the\n way out is the grabber (swipe-to-dismiss) and a backdrop tap. -->\n <div class=\"px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-bold text-base-content truncate\">{{ menuLabel || triggerAriaLabel }}</p>\n </div>\n <ng-container [ngTemplateOutlet]=\"searchBox\" [ngTemplateOutletContext]=\"{ sheet: true }\"></ng-container>\n <div class=\"flex-1 flex flex-col overflow-auto overscroll-contain\">\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: true }\"></ng-container>\n </div>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-menu'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.bottom]=\"dropdownStyle.bottom\"\n [style.max-height]=\"dropdownStyle.maxHeight\"\n [style.height.px]=\"panelFloorPx\"\n [style.width.px]=\"panelWidthPx\"\n role=\"menu\"\n >\n @if (menuLabel) {\n <p class=\"px-3 pt-1 pb-1.5 text-xs font-medium text-base-content/50 truncate shrink-0\">{{ menuLabel }}</p>\n }\n <ng-container [ngTemplateOutlet]=\"searchBox\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n @if (isSearchable) {\n <!-- Its own scroll region so the pinned search box above stays fixed and the\n locked panel height (panelFloorPx) doesn't change as the list filters. The\n flex column lets the empty state fill and centre in the reserved space. -->\n <div class=\"flex-1 min-h-0 flex flex-col overflow-auto\">\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n }\n </div>\n }\n }\n\n <!-- The item list, shared verbatim by the sheet and the anchored popover. `sheet`\n only tunes the spacing so touch targets are roomier on mobile. -->\n <ng-template #items let-sheet=\"sheet\">\n @for (item of filteredActions; track $index) {\n @if (asAction(item); as action) {\n <button\n type=\"button\"\n role=\"menuitem\"\n [disabled]=\"action.disabled\"\n [attr.aria-current]=\"action.active ? 'true' : null\"\n [class.opacity-50]=\"action.disabled\"\n [class.pointer-events-none]=\"action.disabled\"\n [ngClass]=\"[actionColorClass(action), sheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm', action.active ? 'bg-primary/10 font-medium' : '']\"\n class=\"flex w-full shrink-0 items-center gap-x-2.5 text-left cursor-pointer hover:bg-base-200 focus-visible:bg-base-200 focus:outline-none transition-colors\"\n (click)=\"select(action)\"\n >\n @if (action.icon) {\n <span class=\"shrink-0 inline-flex items-center\">\n @if (isTemplateRef(action.icon)) {\n <ng-container [ngTemplateOutlet]=\"action.icon\"></ng-container>\n } @else {\n <!-- Data icon: rendered here so the item sizes it to match its own text\n (larger in the sheet, where the rows are touch-sized). -->\n <svg [lucideIcon]=\"$any(action.icon)\" [size]=\"sheet ? 18 : 16\"></svg>\n }\n </span>\n }\n <span class=\"truncate min-w-0\">{{ actionLabel(action) }}</span>\n <!-- The current choice's marker (e.g. the active language). Decorative: the\n state is conveyed to assistive tech by `aria-current` on the row. -->\n @if (action.active) {\n <svg\n [lucideIcon]=\"checkIcon\"\n [size]=\"sheet ? 18 : 16\"\n class=\"ml-auto shrink-0 text-primary\"\n aria-hidden=\"true\"\n ></svg>\n }\n </button>\n } @else {\n <hr role=\"separator\" [ngClass]=\"sheet ? 'mx-4 my-1.5' : 'mx-2 my-1'\" class=\"shrink-0 border-t border-base-300\" />\n }\n }\n @if (isSearchable && filteredActions.length === 0) {\n <div class=\"flex-1 flex flex-col items-center justify-center gap-2 px-4 py-6 text-center text-base-content/50\">\n <svg lucideSearchX [size]=\"sheet ? 28 : 24\" class=\"opacity-60\"></svg>\n <span [ngClass]=\"sheet ? 'text-base' : 'text-sm'\">{{ searchEmptyLabel }}</span>\n </div>\n }\n </ng-template>\n\n <!-- The filter input, shared by the sheet and the anchored popover. Autofocused on\n desktop (`!sheet`) so typing starts immediately; deliberately not on mobile, where\n it would pop the soft keyboard and fight the viewport-anchored sheet. Enter runs\n the first visible action via the wrapper's bubbled keydown. -->\n <ng-template #searchBox let-sheet=\"sheet\">\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n (keydown.enter)=\"selectFirstVisible()\"\n [ngClass]=\"sheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: searchPlaceholder,\n ariaLabel: searchPlaceholder,\n fullWidth: true,\n size: 'sm',\n autoFocus: !sheet\n }\"\n ></mn-lib-input-field>\n </div>\n }\n </ng-template>\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnBottomSheet, selector: "mn-bottom-sheet", inputs: ["showBackdrop", "showGrabber", "dismissible", "minHeightPx", "maxHeightVh", "containerClass", "ariaLabel", "ariaLabelledby", "growWithKeyboard", "dismissGuard"], outputs: ["dismiss"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: LucideSearchX, selector: "svg[lucideSearchX]" }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }] });
|
|
5486
5538
|
}
|
|
5487
5539
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnDropdown, decorators: [{
|
|
5488
5540
|
type: Component,
|
|
5489
|
-
args: [{ selector: 'mn-lib-dropdown', standalone: true, imports: [NgClass, NgTemplateOutlet, FormsModule, MnButton, MnBottomSheet, MnInputField, LucideSearchX, LucideDynamicIcon], template: "<div class=\"relative inline-flex\">\n <!-- Trigger -->\n <button\n #trigger\n mnButton\n type=\"button\"\n [id]=\"resolvedId\"\n [data]=\"triggerData\"\n [ngClass]=\"triggerClasses\"\n [attr.aria-label]=\"triggerLabelText ? null : triggerAriaLabel\"\n [attr.aria-haspopup]=\"'menu'\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-menu' : null\"\n (click)=\"toggle()\"\n >\n @if (triggerLabelText) {\n <span class=\"truncate\">{{ triggerLabelText }}</span>\n }\n <!-- One glyph, rendered like a menu item's icon: a caller's template, else lucide data\n (a preset resolves to its own data). No per-preset switch. -->\n @if (triggerIconTemplate; as tpl) {\n <span class=\"shrink-0 inline-flex items-center\">\n <ng-container [ngTemplateOutlet]=\"tpl\"></ng-container>\n </span>\n } @else if (triggerIconData; as glyph) {\n <span class=\"shrink-0 inline-flex items-center\">\n <svg [lucideIcon]=\"glyph.data\" [size]=\"glyph.size\" [class.opacity-70]=\"glyph.dim\"></svg>\n </span>\n }\n </button>\n\n <!-- Menu -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the menu is presented as the shared bottom sheet: chrome (backdrop,\n grabber, swipe-to-dismiss, slide animation) lives in mn-bottom-sheet; this\n component only projects the item list into it. The host is portalled to\n document.body (see the `sheet` ViewChild). -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"menuLabel || triggerAriaLabel\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div [id]=\"resolvedId + '-menu'\" role=\"menu\" class=\"flex flex-col flex-1 min-h-0 overflow-hidden\">\n <!-- The sheet covers its own trigger, so it carries a header to name itself; the\n way out is the grabber (swipe-to-dismiss) and a backdrop tap. -->\n <div class=\"px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-bold text-base-content truncate\">{{ menuLabel || triggerAriaLabel }}</p>\n </div>\n <ng-container [ngTemplateOutlet]=\"searchBox\" [ngTemplateOutletContext]=\"{ sheet: true }\"></ng-container>\n <div class=\"flex-1 flex flex-col overflow-auto overscroll-contain\">\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: true }\"></ng-container>\n </div>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-menu'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.height.px]=\"panelFloorPx\"\n [style.width.px]=\"panelWidthPx\"\n role=\"menu\"\n >\n @if (menuLabel) {\n <p class=\"px-3 pt-1 pb-1.5 text-xs font-medium text-base-content/50 truncate shrink-0\">{{ menuLabel }}</p>\n }\n <ng-container [ngTemplateOutlet]=\"searchBox\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n @if (isSearchable) {\n <!-- Its own scroll region so the pinned search box above stays fixed and the\n locked panel height (panelFloorPx) doesn't change as the list filters. The\n flex column lets the empty state fill and centre in the reserved space. -->\n <div class=\"flex-1 min-h-0 flex flex-col overflow-auto\">\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n }\n </div>\n }\n }\n\n <!-- The item list, shared verbatim by the sheet and the anchored popover. `sheet`\n only tunes the spacing so touch targets are roomier on mobile. -->\n <ng-template #items let-sheet=\"sheet\">\n @for (item of filteredActions; track $index) {\n @if (asAction(item); as action) {\n <button\n type=\"button\"\n role=\"menuitem\"\n [disabled]=\"action.disabled\"\n [attr.aria-current]=\"action.active ? 'true' : null\"\n [class.opacity-50]=\"action.disabled\"\n [class.pointer-events-none]=\"action.disabled\"\n [ngClass]=\"[actionColorClass(action), sheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm', action.active ? 'bg-primary/10 font-medium' : '']\"\n class=\"flex w-full shrink-0 items-center gap-x-2.5 text-left cursor-pointer hover:bg-base-200 focus-visible:bg-base-200 focus:outline-none transition-colors\"\n (click)=\"select(action)\"\n >\n @if (action.icon) {\n <span class=\"shrink-0 inline-flex items-center\">\n @if (isTemplateRef(action.icon)) {\n <ng-container [ngTemplateOutlet]=\"action.icon\"></ng-container>\n } @else {\n <!-- Data icon: rendered here so the item sizes it to match its own text\n (larger in the sheet, where the rows are touch-sized). -->\n <svg [lucideIcon]=\"$any(action.icon)\" [size]=\"sheet ? 18 : 16\"></svg>\n }\n </span>\n }\n <span class=\"truncate min-w-0\">{{ actionLabel(action) }}</span>\n <!-- The current choice's marker (e.g. the active language). Decorative: the\n state is conveyed to assistive tech by `aria-current` on the row. -->\n @if (action.active) {\n <svg\n [lucideIcon]=\"checkIcon\"\n [size]=\"sheet ? 18 : 16\"\n class=\"ml-auto shrink-0 text-primary\"\n aria-hidden=\"true\"\n ></svg>\n }\n </button>\n } @else {\n <hr role=\"separator\" [ngClass]=\"sheet ? 'mx-4 my-1.5' : 'mx-2 my-1'\" class=\"shrink-0 border-t border-base-300\" />\n }\n }\n @if (isSearchable && filteredActions.length === 0) {\n <div class=\"flex-1 flex flex-col items-center justify-center gap-2 px-4 py-6 text-center text-base-content/50\">\n <svg lucideSearchX [size]=\"sheet ? 28 : 24\" class=\"opacity-60\"></svg>\n <span [ngClass]=\"sheet ? 'text-base' : 'text-sm'\">{{ searchEmptyLabel }}</span>\n </div>\n }\n </ng-template>\n\n <!-- The filter input, shared by the sheet and the anchored popover. Autofocused on\n desktop (`!sheet`) so typing starts immediately; deliberately not on mobile, where\n it would pop the soft keyboard and fight the viewport-anchored sheet. Enter runs\n the first visible action via the wrapper's bubbled keydown. -->\n <ng-template #searchBox let-sheet=\"sheet\">\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n (keydown.enter)=\"selectFirstVisible()\"\n [ngClass]=\"sheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: searchPlaceholder,\n ariaLabel: searchPlaceholder,\n fullWidth: true,\n size: 'sm',\n autoFocus: !sheet\n }\"\n ></mn-lib-input-field>\n </div>\n }\n </ng-template>\n</div>\n" }]
|
|
5541
|
+
args: [{ selector: 'mn-lib-dropdown', standalone: true, imports: [NgClass, NgTemplateOutlet, FormsModule, MnButton, MnBottomSheet, MnInputField, LucideSearchX, LucideDynamicIcon], template: "<div class=\"relative inline-flex\">\n <!-- Trigger -->\n <button\n #trigger\n mnButton\n type=\"button\"\n [id]=\"resolvedId\"\n [data]=\"triggerData\"\n [ngClass]=\"triggerClasses\"\n [attr.aria-label]=\"triggerLabelText ? null : triggerAriaLabel\"\n [attr.aria-haspopup]=\"'menu'\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-menu' : null\"\n (click)=\"toggle()\"\n >\n @if (triggerLabelText) {\n <span class=\"truncate\">{{ triggerLabelText }}</span>\n }\n <!-- One glyph, rendered like a menu item's icon: a caller's template, else lucide data\n (a preset resolves to its own data). No per-preset switch. -->\n @if (triggerIconTemplate; as tpl) {\n <span class=\"shrink-0 inline-flex items-center\">\n <ng-container [ngTemplateOutlet]=\"tpl\"></ng-container>\n </span>\n } @else if (triggerIconData; as glyph) {\n <span class=\"shrink-0 inline-flex items-center\">\n <svg [lucideIcon]=\"glyph.data\" [size]=\"glyph.size\" [class.opacity-70]=\"glyph.dim\"></svg>\n </span>\n }\n </button>\n\n <!-- Menu -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the menu is presented as the shared bottom sheet: chrome (backdrop,\n grabber, swipe-to-dismiss, slide animation) lives in mn-bottom-sheet; this\n component only projects the item list into it. The host is portalled to\n document.body (see the `sheet` ViewChild). -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"menuLabel || triggerAriaLabel\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div [id]=\"resolvedId + '-menu'\" role=\"menu\" class=\"flex flex-col flex-1 min-h-0 overflow-hidden\">\n <!-- The sheet covers its own trigger, so it carries a header to name itself; the\n way out is the grabber (swipe-to-dismiss) and a backdrop tap. -->\n <div class=\"px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-bold text-base-content truncate\">{{ menuLabel || triggerAriaLabel }}</p>\n </div>\n <ng-container [ngTemplateOutlet]=\"searchBox\" [ngTemplateOutletContext]=\"{ sheet: true }\"></ng-container>\n <div class=\"flex-1 flex flex-col overflow-auto overscroll-contain\">\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: true }\"></ng-container>\n </div>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-menu'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.bottom]=\"dropdownStyle.bottom\"\n [style.max-height]=\"dropdownStyle.maxHeight\"\n [style.height.px]=\"panelFloorPx\"\n [style.width.px]=\"panelWidthPx\"\n role=\"menu\"\n >\n @if (menuLabel) {\n <p class=\"px-3 pt-1 pb-1.5 text-xs font-medium text-base-content/50 truncate shrink-0\">{{ menuLabel }}</p>\n }\n <ng-container [ngTemplateOutlet]=\"searchBox\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n @if (isSearchable) {\n <!-- Its own scroll region so the pinned search box above stays fixed and the\n locked panel height (panelFloorPx) doesn't change as the list filters. The\n flex column lets the empty state fill and centre in the reserved space. -->\n <div class=\"flex-1 min-h-0 flex flex-col overflow-auto\">\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n </div>\n } @else {\n <ng-container [ngTemplateOutlet]=\"items\" [ngTemplateOutletContext]=\"{ sheet: false }\"></ng-container>\n }\n </div>\n }\n }\n\n <!-- The item list, shared verbatim by the sheet and the anchored popover. `sheet`\n only tunes the spacing so touch targets are roomier on mobile. -->\n <ng-template #items let-sheet=\"sheet\">\n @for (item of filteredActions; track $index) {\n @if (asAction(item); as action) {\n <button\n type=\"button\"\n role=\"menuitem\"\n [disabled]=\"action.disabled\"\n [attr.aria-current]=\"action.active ? 'true' : null\"\n [class.opacity-50]=\"action.disabled\"\n [class.pointer-events-none]=\"action.disabled\"\n [ngClass]=\"[actionColorClass(action), sheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm', action.active ? 'bg-primary/10 font-medium' : '']\"\n class=\"flex w-full shrink-0 items-center gap-x-2.5 text-left cursor-pointer hover:bg-base-200 focus-visible:bg-base-200 focus:outline-none transition-colors\"\n (click)=\"select(action)\"\n >\n @if (action.icon) {\n <span class=\"shrink-0 inline-flex items-center\">\n @if (isTemplateRef(action.icon)) {\n <ng-container [ngTemplateOutlet]=\"action.icon\"></ng-container>\n } @else {\n <!-- Data icon: rendered here so the item sizes it to match its own text\n (larger in the sheet, where the rows are touch-sized). -->\n <svg [lucideIcon]=\"$any(action.icon)\" [size]=\"sheet ? 18 : 16\"></svg>\n }\n </span>\n }\n <span class=\"truncate min-w-0\">{{ actionLabel(action) }}</span>\n <!-- The current choice's marker (e.g. the active language). Decorative: the\n state is conveyed to assistive tech by `aria-current` on the row. -->\n @if (action.active) {\n <svg\n [lucideIcon]=\"checkIcon\"\n [size]=\"sheet ? 18 : 16\"\n class=\"ml-auto shrink-0 text-primary\"\n aria-hidden=\"true\"\n ></svg>\n }\n </button>\n } @else {\n <hr role=\"separator\" [ngClass]=\"sheet ? 'mx-4 my-1.5' : 'mx-2 my-1'\" class=\"shrink-0 border-t border-base-300\" />\n }\n }\n @if (isSearchable && filteredActions.length === 0) {\n <div class=\"flex-1 flex flex-col items-center justify-center gap-2 px-4 py-6 text-center text-base-content/50\">\n <svg lucideSearchX [size]=\"sheet ? 28 : 24\" class=\"opacity-60\"></svg>\n <span [ngClass]=\"sheet ? 'text-base' : 'text-sm'\">{{ searchEmptyLabel }}</span>\n </div>\n }\n </ng-template>\n\n <!-- The filter input, shared by the sheet and the anchored popover. Autofocused on\n desktop (`!sheet`) so typing starts immediately; deliberately not on mobile, where\n it would pop the soft keyboard and fight the viewport-anchored sheet. Enter runs\n the first visible action via the wrapper's bubbled keydown. -->\n <ng-template #searchBox let-sheet=\"sheet\">\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n (keydown.enter)=\"selectFirstVisible()\"\n [ngClass]=\"sheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: searchPlaceholder,\n ariaLabel: searchPlaceholder,\n fullWidth: true,\n size: 'sm',\n autoFocus: !sheet\n }\"\n ></mn-lib-input-field>\n </div>\n }\n </ng-template>\n</div>\n" }]
|
|
5490
5542
|
}], propDecorators: { datasource: [{
|
|
5491
5543
|
type: Input,
|
|
5492
5544
|
args: [{ required: true }]
|
|
@@ -5758,6 +5810,8 @@ class MnSelect {
|
|
|
5758
5810
|
/** Layout classes for the anchored popover panel. The mobile sheet is rendered by
|
|
5759
5811
|
* mn-bottom-sheet instead, so it no longer needs a branch here. */
|
|
5760
5812
|
panelClasses = 'fixed z-9999 bg-base-100 border border-base-300 rounded-md shadow-lg max-h-60 overflow-auto';
|
|
5813
|
+
/** The panel's own height cap in pixels: the `max-h-60` above, restated for the placement maths. */
|
|
5814
|
+
static PANEL_MAX_HEIGHT_PX = 240;
|
|
5761
5815
|
/** Layout classes for the invisible click shield rendered under the anchored panel.
|
|
5762
5816
|
* One step below the panel's z-index so the panel itself stays clickable, and above
|
|
5763
5817
|
* any modal/drawer chrome (which tops out well under 9998). */
|
|
@@ -5800,7 +5854,14 @@ class MnSelect {
|
|
|
5800
5854
|
*/
|
|
5801
5855
|
scrollCapture = null;
|
|
5802
5856
|
/** Dropdown position calculated from the trigger's bounding rect. */
|
|
5803
|
-
|
|
5857
|
+
/** Inline placement of the anchored panel; `maxHeight` only binds when the viewport is the tighter cap. */
|
|
5858
|
+
dropdownStyle = {
|
|
5859
|
+
top: '0px',
|
|
5860
|
+
bottom: 'auto',
|
|
5861
|
+
left: '0px',
|
|
5862
|
+
width: '0px',
|
|
5863
|
+
maxHeight: null,
|
|
5864
|
+
};
|
|
5804
5865
|
onChange = () => {
|
|
5805
5866
|
};
|
|
5806
5867
|
onTouched = () => {
|
|
@@ -6117,13 +6178,16 @@ class MnSelect {
|
|
|
6117
6178
|
this.previousBodyOverflow = null;
|
|
6118
6179
|
}
|
|
6119
6180
|
// ========== Positioning ==========
|
|
6120
|
-
/**
|
|
6181
|
+
/**
|
|
6182
|
+
* Calculates the fixed position for the dropdown based on the trigger element: below it
|
|
6183
|
+
* while the viewport has room, above it otherwise, never past the viewport's edge.
|
|
6184
|
+
*/
|
|
6121
6185
|
updateDropdownPosition() {
|
|
6122
6186
|
if (!this.triggerRef)
|
|
6123
6187
|
return;
|
|
6124
6188
|
const rect = this.triggerRef.nativeElement.getBoundingClientRect();
|
|
6125
6189
|
this.dropdownStyle = {
|
|
6126
|
-
|
|
6190
|
+
...anchoredPanelPlacement(rect, window.innerHeight, 0, MnSelect.PANEL_MAX_HEIGHT_PX),
|
|
6127
6191
|
left: `${rect.left}px`,
|
|
6128
6192
|
width: `${rect.width}px`,
|
|
6129
6193
|
};
|
|
@@ -6258,7 +6322,7 @@ class MnSelect {
|
|
|
6258
6322
|
return msgDef;
|
|
6259
6323
|
}
|
|
6260
6324
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6261
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnSelect, isStandalone: true, selector: "mn-lib-select", inputs: { props: "props" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()", "window:scroll": "onWindowScrollOrResize()", "window:resize": "onWindowScrollOrResize()" }, properties: { "style.display": "props?.fullWidth ? 'block' : null", "style.width": "props?.fullWidth ? '100%' : null" } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true }, { propertyName: "dropdownRef", first: true, predicate: ["dropdown"], descendants: true }, { propertyName: "shieldRef", first: true, predicate: ["shield"], descendants: true }, { propertyName: "sheetRef", first: true, predicate: ["sheet"], descendants: true, read: ElementRef }], ngImport: i0, template: "<div [class.is-fullwidth]=\"props.fullWidth\" class=\"flex flex-col h-full\">\n @if (uiConfig.label || props.label) {\n <label [attr.for]=\"resolvedId\" class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Trigger -->\n <div\n #trigger\n (blur)=\"handleBlur()\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-listbox' : null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-disabled]=\"isDisabled || null\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [class.cursor-not-allowed]=\"isDisabled\"\n [class.opacity-60]=\"isDisabled\"\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n [tabindex]=\"isDisabled ? -1 : 0\"\n aria-haspopup=\"listbox\"\n class=\"relative\"\n role=\"combobox\"\n >\n <!-- `pr-6` reserves the gutter the caret is absolutely positioned in (right-2 + w-4),\n so the value can never render underneath it. `min-w-0` lets the label shrink below\n its content width, which is what makes truncation possible. -->\n <div class=\"flex flex-row items-center min-h-6 min-w-0 pr-6\">\n <span\n [attr.title]=\"selectedOption?.label\"\n [ngClass]=\"selectedOption ? 'text-base-content' : 'text-base-content/50'\"\n class=\"truncate\"\n >{{ displayText }}</span>\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg [size]=\"16\" class=\"text-base-content/50\" lucideChevronDown></svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the panel is presented as a shared bottom sheet: the sheet chrome\n (backdrop, grabber, swipe/flick-to-dismiss, slide animation) lives in\n mn-bottom-sheet; this component only projects the field's content into it. The\n sheet host is portalled to document.body (see the `sheet` ViewChild) so its\n `position: fixed` anchors to the viewport, not a transformed ancestor. -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"uiConfig.ariaLabel || uiConfig.label || props.label || uiConfig.placeholder || props.placeholder\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div\n [id]=\"resolvedId + '-listbox'\"\n class=\"flex flex-col flex-1 min-h-0 overflow-hidden\"\n role=\"listbox\"\n >\n <!-- The sheet covers its own trigger, so it carries a header to name the field; the\n way out is the grabber (swipe-to-dismiss) and a backdrop tap. -->\n <div class=\"px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-medium text-base-content truncate\">\n {{ uiConfig.label || props.label || uiConfig.placeholder || props.placeholder || '' }}\n </p>\n </div>\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- A transparent full-viewport shield behind the panel, so \"click anywhere to dismiss\"\n is literally true. It also *consumes* that click: without it the click reaches\n whatever sits underneath \u2014 inside a modal that is the modal's own backdrop, so\n dismissing the dropdown would tear down the whole modal with it. Portalled to\n document.body for the same reason the panel is. It is aria-hidden and unfocusable:\n the keyboard equivalent of this click is Escape. -->\n <div\n #shield\n (click)=\"onShieldClick($event)\"\n [id]=\"resolvedId + '-shield'\"\n [ngClass]=\"shieldClasses\"\n aria-hidden=\"true\"\n ></div>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-listbox'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.width]=\"dropdownStyle.width\"\n role=\"listbox\"\n >\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n }\n }\n\n <!-- The search box + option list, shared verbatim by the sheet and the anchored popover.\n `isSheet` only tunes spacing/sizing and which element scrolls: in sheet mode the list\n is the flex scroller; anchored, the popover itself scrolls. -->\n <ng-template #panelBody>\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n [ngClass]=\"isSheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: searchPlaceholderLabel,\n ariaLabel: searchPlaceholderLabel,\n fullWidth: true,\n size: 'sm',\n autoFocus: !isSheet\n }\"\n ></mn-lib-input-field>\n </div>\n }\n <div [ngClass]=\"isSheet ? 'flex-1 overflow-auto overscroll-contain' : ''\">\n @for (opt of filteredOptions; track opt.value) {\n <div\n (click)=\"selectOption(opt); $event.stopPropagation()\"\n (keyup.enter)=\"selectOption(opt)\"\n (keyup.space)=\"selectOption(opt)\"\n [attr.aria-selected]=\"isSelected(opt)\"\n [class.opacity-50]=\"opt.disabled\"\n [class.pointer-events-none]=\"opt.disabled\"\n [ngClass]=\"[isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm', isSelected(opt) ? 'bg-primary/10 font-medium' : '']\"\n class=\"flex items-center gap-x-2.5 cursor-pointer text-base-content hover:bg-base-200 transition-colors\"\n role=\"option\"\n tabindex=\"0\"\n >\n <span class=\"truncate min-w-0\">{{ opt.label }}</span>\n <!-- The current choice's marker. Decorative: the state is conveyed to assistive\n tech by `aria-selected` on the row. -->\n @if (isSelected(opt)) {\n <svg\n [lucideIcon]=\"checkIcon\"\n [size]=\"isSheet ? 18 : 16\"\n aria-hidden=\"true\"\n class=\"ml-auto shrink-0 text-primary\"\n ></svg>\n }\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\" class=\"text-base-content/50\">\n {{ noOptionsLabel }}\n </div>\n }\n </div>\n </ng-template>\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n }\n }\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnErrorMessage, selector: "mn-error-message", inputs: ["errorMessage", "id"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnBottomSheet, selector: "mn-bottom-sheet", inputs: ["showBackdrop", "showGrabber", "dismissible", "minHeightPx", "maxHeightVh", "containerClass", "ariaLabel", "ariaLabelledby", "growWithKeyboard", "dismissGuard"], outputs: ["dismiss"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }, { kind: "component", type: LucideChevronDown, selector: "svg[lucideChevronDown]" }] });
|
|
6325
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnSelect, isStandalone: true, selector: "mn-lib-select", inputs: { props: "props" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()", "window:scroll": "onWindowScrollOrResize()", "window:resize": "onWindowScrollOrResize()" }, properties: { "style.display": "props?.fullWidth ? 'block' : null", "style.width": "props?.fullWidth ? '100%' : null" } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true }, { propertyName: "dropdownRef", first: true, predicate: ["dropdown"], descendants: true }, { propertyName: "shieldRef", first: true, predicate: ["shield"], descendants: true }, { propertyName: "sheetRef", first: true, predicate: ["sheet"], descendants: true, read: ElementRef }], ngImport: i0, template: "<div [class.is-fullwidth]=\"props.fullWidth\" class=\"flex flex-col h-full\">\n @if (uiConfig.label || props.label) {\n <label [attr.for]=\"resolvedId\" class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Trigger -->\n <div\n #trigger\n (blur)=\"handleBlur()\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-listbox' : null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-disabled]=\"isDisabled || null\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [class.cursor-not-allowed]=\"isDisabled\"\n [class.opacity-60]=\"isDisabled\"\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n [tabindex]=\"isDisabled ? -1 : 0\"\n aria-haspopup=\"listbox\"\n class=\"relative\"\n role=\"combobox\"\n >\n <!-- `pr-6` reserves the gutter the caret is absolutely positioned in (right-2 + w-4),\n so the value can never render underneath it. `min-w-0` lets the label shrink below\n its content width, which is what makes truncation possible. -->\n <div class=\"flex flex-row items-center min-h-6 min-w-0 pr-6\">\n <span\n [attr.title]=\"selectedOption?.label\"\n [ngClass]=\"selectedOption ? 'text-base-content' : 'text-base-content/50'\"\n class=\"truncate\"\n >{{ displayText }}</span>\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg [size]=\"16\" class=\"text-base-content/50\" lucideChevronDown></svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the panel is presented as a shared bottom sheet: the sheet chrome\n (backdrop, grabber, swipe/flick-to-dismiss, slide animation) lives in\n mn-bottom-sheet; this component only projects the field's content into it. The\n sheet host is portalled to document.body (see the `sheet` ViewChild) so its\n `position: fixed` anchors to the viewport, not a transformed ancestor. -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"uiConfig.ariaLabel || uiConfig.label || props.label || uiConfig.placeholder || props.placeholder\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div\n [id]=\"resolvedId + '-listbox'\"\n class=\"flex flex-col flex-1 min-h-0 overflow-hidden\"\n role=\"listbox\"\n >\n <!-- The sheet covers its own trigger, so it carries a header to name the field; the\n way out is the grabber (swipe-to-dismiss) and a backdrop tap. -->\n <div class=\"px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-medium text-base-content truncate\">\n {{ uiConfig.label || props.label || uiConfig.placeholder || props.placeholder || '' }}\n </p>\n </div>\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- A transparent full-viewport shield behind the panel, so \"click anywhere to dismiss\"\n is literally true. It also *consumes* that click: without it the click reaches\n whatever sits underneath \u2014 inside a modal that is the modal's own backdrop, so\n dismissing the dropdown would tear down the whole modal with it. Portalled to\n document.body for the same reason the panel is. It is aria-hidden and unfocusable:\n the keyboard equivalent of this click is Escape. -->\n <div\n #shield\n (click)=\"onShieldClick($event)\"\n [id]=\"resolvedId + '-shield'\"\n [ngClass]=\"shieldClasses\"\n aria-hidden=\"true\"\n ></div>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-listbox'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.bottom]=\"dropdownStyle.bottom\"\n [style.max-height]=\"dropdownStyle.maxHeight\"\n [style.width]=\"dropdownStyle.width\"\n role=\"listbox\"\n >\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n }\n }\n\n <!-- The search box + option list, shared verbatim by the sheet and the anchored popover.\n `isSheet` only tunes spacing/sizing and which element scrolls: in sheet mode the list\n is the flex scroller; anchored, the popover itself scrolls. -->\n <ng-template #panelBody>\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n [ngClass]=\"isSheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: searchPlaceholderLabel,\n ariaLabel: searchPlaceholderLabel,\n fullWidth: true,\n size: 'sm',\n autoFocus: !isSheet\n }\"\n ></mn-lib-input-field>\n </div>\n }\n <div [ngClass]=\"isSheet ? 'flex-1 overflow-auto overscroll-contain' : ''\">\n @for (opt of filteredOptions; track opt.value) {\n <div\n (click)=\"selectOption(opt); $event.stopPropagation()\"\n (keyup.enter)=\"selectOption(opt)\"\n (keyup.space)=\"selectOption(opt)\"\n [attr.aria-selected]=\"isSelected(opt)\"\n [class.opacity-50]=\"opt.disabled\"\n [class.pointer-events-none]=\"opt.disabled\"\n [ngClass]=\"[isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm', isSelected(opt) ? 'bg-primary/10 font-medium' : '']\"\n class=\"flex items-center gap-x-2.5 cursor-pointer text-base-content hover:bg-base-200 transition-colors\"\n role=\"option\"\n tabindex=\"0\"\n >\n <span class=\"truncate min-w-0\">{{ opt.label }}</span>\n <!-- The current choice's marker. Decorative: the state is conveyed to assistive\n tech by `aria-selected` on the row. -->\n @if (isSelected(opt)) {\n <svg\n [lucideIcon]=\"checkIcon\"\n [size]=\"isSheet ? 18 : 16\"\n aria-hidden=\"true\"\n class=\"ml-auto shrink-0 text-primary\"\n ></svg>\n }\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\" class=\"text-base-content/50\">\n {{ noOptionsLabel }}\n </div>\n }\n </div>\n </ng-template>\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n }\n }\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnErrorMessage, selector: "mn-error-message", inputs: ["errorMessage", "id"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnBottomSheet, selector: "mn-bottom-sheet", inputs: ["showBackdrop", "showGrabber", "dismissible", "minHeightPx", "maxHeightVh", "containerClass", "ariaLabel", "ariaLabelledby", "growWithKeyboard", "dismissGuard"], outputs: ["dismiss"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }, { kind: "component", type: LucideChevronDown, selector: "svg[lucideChevronDown]" }] });
|
|
6262
6326
|
}
|
|
6263
6327
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSelect, decorators: [{
|
|
6264
6328
|
type: Component,
|
|
@@ -6268,7 +6332,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
6268
6332
|
// to fill the parent. Give the host a real width when fullWidth is requested.
|
|
6269
6333
|
'[style.display]': "props?.fullWidth ? 'block' : null",
|
|
6270
6334
|
'[style.width]': "props?.fullWidth ? '100%' : null",
|
|
6271
|
-
}, template: "<div [class.is-fullwidth]=\"props.fullWidth\" class=\"flex flex-col h-full\">\n @if (uiConfig.label || props.label) {\n <label [attr.for]=\"resolvedId\" class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Trigger -->\n <div\n #trigger\n (blur)=\"handleBlur()\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-listbox' : null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-disabled]=\"isDisabled || null\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [class.cursor-not-allowed]=\"isDisabled\"\n [class.opacity-60]=\"isDisabled\"\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n [tabindex]=\"isDisabled ? -1 : 0\"\n aria-haspopup=\"listbox\"\n class=\"relative\"\n role=\"combobox\"\n >\n <!-- `pr-6` reserves the gutter the caret is absolutely positioned in (right-2 + w-4),\n so the value can never render underneath it. `min-w-0` lets the label shrink below\n its content width, which is what makes truncation possible. -->\n <div class=\"flex flex-row items-center min-h-6 min-w-0 pr-6\">\n <span\n [attr.title]=\"selectedOption?.label\"\n [ngClass]=\"selectedOption ? 'text-base-content' : 'text-base-content/50'\"\n class=\"truncate\"\n >{{ displayText }}</span>\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg [size]=\"16\" class=\"text-base-content/50\" lucideChevronDown></svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the panel is presented as a shared bottom sheet: the sheet chrome\n (backdrop, grabber, swipe/flick-to-dismiss, slide animation) lives in\n mn-bottom-sheet; this component only projects the field's content into it. The\n sheet host is portalled to document.body (see the `sheet` ViewChild) so its\n `position: fixed` anchors to the viewport, not a transformed ancestor. -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"uiConfig.ariaLabel || uiConfig.label || props.label || uiConfig.placeholder || props.placeholder\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div\n [id]=\"resolvedId + '-listbox'\"\n class=\"flex flex-col flex-1 min-h-0 overflow-hidden\"\n role=\"listbox\"\n >\n <!-- The sheet covers its own trigger, so it carries a header to name the field; the\n way out is the grabber (swipe-to-dismiss) and a backdrop tap. -->\n <div class=\"px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-medium text-base-content truncate\">\n {{ uiConfig.label || props.label || uiConfig.placeholder || props.placeholder || '' }}\n </p>\n </div>\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- A transparent full-viewport shield behind the panel, so \"click anywhere to dismiss\"\n is literally true. It also *consumes* that click: without it the click reaches\n whatever sits underneath \u2014 inside a modal that is the modal's own backdrop, so\n dismissing the dropdown would tear down the whole modal with it. Portalled to\n document.body for the same reason the panel is. It is aria-hidden and unfocusable:\n the keyboard equivalent of this click is Escape. -->\n <div\n #shield\n (click)=\"onShieldClick($event)\"\n [id]=\"resolvedId + '-shield'\"\n [ngClass]=\"shieldClasses\"\n aria-hidden=\"true\"\n ></div>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-listbox'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.width]=\"dropdownStyle.width\"\n role=\"listbox\"\n >\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n }\n }\n\n <!-- The search box + option list, shared verbatim by the sheet and the anchored popover.\n `isSheet` only tunes spacing/sizing and which element scrolls: in sheet mode the list\n is the flex scroller; anchored, the popover itself scrolls. -->\n <ng-template #panelBody>\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n [ngClass]=\"isSheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: searchPlaceholderLabel,\n ariaLabel: searchPlaceholderLabel,\n fullWidth: true,\n size: 'sm',\n autoFocus: !isSheet\n }\"\n ></mn-lib-input-field>\n </div>\n }\n <div [ngClass]=\"isSheet ? 'flex-1 overflow-auto overscroll-contain' : ''\">\n @for (opt of filteredOptions; track opt.value) {\n <div\n (click)=\"selectOption(opt); $event.stopPropagation()\"\n (keyup.enter)=\"selectOption(opt)\"\n (keyup.space)=\"selectOption(opt)\"\n [attr.aria-selected]=\"isSelected(opt)\"\n [class.opacity-50]=\"opt.disabled\"\n [class.pointer-events-none]=\"opt.disabled\"\n [ngClass]=\"[isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm', isSelected(opt) ? 'bg-primary/10 font-medium' : '']\"\n class=\"flex items-center gap-x-2.5 cursor-pointer text-base-content hover:bg-base-200 transition-colors\"\n role=\"option\"\n tabindex=\"0\"\n >\n <span class=\"truncate min-w-0\">{{ opt.label }}</span>\n <!-- The current choice's marker. Decorative: the state is conveyed to assistive\n tech by `aria-selected` on the row. -->\n @if (isSelected(opt)) {\n <svg\n [lucideIcon]=\"checkIcon\"\n [size]=\"isSheet ? 18 : 16\"\n aria-hidden=\"true\"\n class=\"ml-auto shrink-0 text-primary\"\n ></svg>\n }\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\" class=\"text-base-content/50\">\n {{ noOptionsLabel }}\n </div>\n }\n </div>\n </ng-template>\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n }\n }\n }\n</div>\n" }]
|
|
6335
|
+
}, template: "<div [class.is-fullwidth]=\"props.fullWidth\" class=\"flex flex-col h-full\">\n @if (uiConfig.label || props.label) {\n <label [attr.for]=\"resolvedId\" class=\"pl-2 pb-1 flex flex-row gap-x-0.5! text-base!\">\n <p>{{ uiConfig.label || props.label }}</p>\n @if (isRequired()) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <!-- Trigger -->\n <div\n #trigger\n (blur)=\"handleBlur()\"\n (click)=\"toggle()\"\n (keydown.enter)=\"toggle()\"\n (keydown.space)=\"toggle(); $event.preventDefault()\"\n [attr.aria-controls]=\"isOpen ? resolvedId + '-listbox' : null\"\n [attr.aria-describedby]=\"showError ? resolvedId + '-error' : null\"\n [attr.aria-disabled]=\"isDisabled || null\"\n [attr.aria-expanded]=\"isOpen\"\n [attr.aria-invalid]=\"showError || null\"\n [attr.aria-label]=\"uiConfig.ariaLabel || uiConfig.label || props.label || null\"\n [class.cursor-not-allowed]=\"isDisabled\"\n [class.opacity-60]=\"isDisabled\"\n [id]=\"resolvedId\"\n [ngClass]=\"triggerClasses\"\n [tabindex]=\"isDisabled ? -1 : 0\"\n aria-haspopup=\"listbox\"\n class=\"relative\"\n role=\"combobox\"\n >\n <!-- `pr-6` reserves the gutter the caret is absolutely positioned in (right-2 + w-4),\n so the value can never render underneath it. `min-w-0` lets the label shrink below\n its content width, which is what makes truncation possible. -->\n <div class=\"flex flex-row items-center min-h-6 min-w-0 pr-6\">\n <span\n [attr.title]=\"selectedOption?.label\"\n [ngClass]=\"selectedOption ? 'text-base-content' : 'text-base-content/50'\"\n class=\"truncate\"\n >{{ displayText }}</span>\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none\">\n <svg [size]=\"16\" class=\"text-base-content/50\" lucideChevronDown></svg>\n </div>\n </div>\n\n <!-- Dropdown -->\n @if (isOpen) {\n @if (isSheet) {\n <!-- On mobile the panel is presented as a shared bottom sheet: the sheet chrome\n (backdrop, grabber, swipe/flick-to-dismiss, slide animation) lives in\n mn-bottom-sheet; this component only projects the field's content into it. The\n sheet host is portalled to document.body (see the `sheet` ViewChild) so its\n `position: fixed` anchors to the viewport, not a transformed ancestor. -->\n <mn-bottom-sheet\n #sheet\n (dismiss)=\"close()\"\n [ariaLabel]=\"uiConfig.ariaLabel || uiConfig.label || props.label || uiConfig.placeholder || props.placeholder\"\n [maxHeightVh]=\"80\"\n [minHeightPx]=\"sheetFloorPx\"\n >\n <div\n [id]=\"resolvedId + '-listbox'\"\n class=\"flex flex-col flex-1 min-h-0 overflow-hidden\"\n role=\"listbox\"\n >\n <!-- The sheet covers its own trigger, so it carries a header to name the field; the\n way out is the grabber (swipe-to-dismiss) and a backdrop tap. -->\n <div class=\"px-4 pt-1 pb-2 shrink-0\">\n <p class=\"text-base font-medium text-base-content truncate\">\n {{ uiConfig.label || props.label || uiConfig.placeholder || props.placeholder || '' }}\n </p>\n </div>\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n </mn-bottom-sheet>\n } @else {\n <!-- A transparent full-viewport shield behind the panel, so \"click anywhere to dismiss\"\n is literally true. It also *consumes* that click: without it the click reaches\n whatever sits underneath \u2014 inside a modal that is the modal's own backdrop, so\n dismissing the dropdown would tear down the whole modal with it. Portalled to\n document.body for the same reason the panel is. It is aria-hidden and unfocusable:\n the keyboard equivalent of this click is Escape. -->\n <div\n #shield\n (click)=\"onShieldClick($event)\"\n [id]=\"resolvedId + '-shield'\"\n [ngClass]=\"shieldClasses\"\n aria-hidden=\"true\"\n ></div>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n #dropdown\n (click)=\"$event.stopPropagation()\"\n [id]=\"resolvedId + '-listbox'\"\n [ngClass]=\"panelClasses\"\n [style.left]=\"dropdownStyle.left\"\n [style.top]=\"dropdownStyle.top\"\n [style.bottom]=\"dropdownStyle.bottom\"\n [style.max-height]=\"dropdownStyle.maxHeight\"\n [style.width]=\"dropdownStyle.width\"\n role=\"listbox\"\n >\n <ng-container [ngTemplateOutlet]=\"panelBody\"></ng-container>\n </div>\n }\n }\n\n <!-- The search box + option list, shared verbatim by the sheet and the anchored popover.\n `isSheet` only tunes spacing/sizing and which element scrolls: in sheet mode the list\n is the flex scroller; anchored, the popover itself scrolls. -->\n <ng-template #panelBody>\n @if (isSearchable) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div\n (click)=\"$event.stopPropagation()\"\n [ngClass]=\"isSheet ? 'px-4 py-2' : 'p-2'\"\n class=\"border-b border-base-300 shrink-0\"\n >\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModelOptions]=\"{ standalone: true }\"\n [ngModel]=\"searchTerm\"\n [props]=\"{\n id: resolvedId + '-search',\n type: 'search',\n placeholder: searchPlaceholderLabel,\n ariaLabel: searchPlaceholderLabel,\n fullWidth: true,\n size: 'sm',\n autoFocus: !isSheet\n }\"\n ></mn-lib-input-field>\n </div>\n }\n <div [ngClass]=\"isSheet ? 'flex-1 overflow-auto overscroll-contain' : ''\">\n @for (opt of filteredOptions; track opt.value) {\n <div\n (click)=\"selectOption(opt); $event.stopPropagation()\"\n (keyup.enter)=\"selectOption(opt)\"\n (keyup.space)=\"selectOption(opt)\"\n [attr.aria-selected]=\"isSelected(opt)\"\n [class.opacity-50]=\"opt.disabled\"\n [class.pointer-events-none]=\"opt.disabled\"\n [ngClass]=\"[isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm', isSelected(opt) ? 'bg-primary/10 font-medium' : '']\"\n class=\"flex items-center gap-x-2.5 cursor-pointer text-base-content hover:bg-base-200 transition-colors\"\n role=\"option\"\n tabindex=\"0\"\n >\n <span class=\"truncate min-w-0\">{{ opt.label }}</span>\n <!-- The current choice's marker. Decorative: the state is conveyed to assistive\n tech by `aria-selected` on the row. -->\n @if (isSelected(opt)) {\n <svg\n [lucideIcon]=\"checkIcon\"\n [size]=\"isSheet ? 18 : 16\"\n aria-hidden=\"true\"\n class=\"ml-auto shrink-0 text-primary\"\n ></svg>\n }\n </div>\n }\n @if (filteredOptions.length === 0) {\n <div [ngClass]=\"isSheet ? 'px-4 py-3 text-base' : 'px-3 py-2 text-sm'\" class=\"text-base-content/50\">\n {{ noOptionsLabel }}\n </div>\n }\n </div>\n </ng-template>\n\n @if (showError) {\n @if (props.showAllErrors) {\n <div class=\"flex flex-col gap-y-1 mt-1\">\n @for (error of errorMessages; track $index) {\n <mn-error-message [errorMessage]=\"error\" [id]=\"resolvedId + '-' + $index\"></mn-error-message>\n }\n </div>\n } @else {\n @if (errorMessage !== null) {\n <mn-error-message [errorMessage]=\"errorMessage\" [id]=\"resolvedId\"></mn-error-message>\n }\n }\n }\n</div>\n" }]
|
|
6272
6336
|
}], ctorParameters: () => [], propDecorators: { props: [{
|
|
6273
6337
|
type: Input,
|
|
6274
6338
|
args: [{ required: true }]
|