mn-angular-lib 1.0.139 → 1.0.140

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mn-angular-lib",
3
- "version": "1.0.139",
3
+ "version": "1.0.140",
4
4
  "peerDependencies": {
5
5
  "@angular/common": "^21.1.3",
6
6
  "@angular/core": "^21.1.3",
@@ -0,0 +1,56 @@
1
+ /*
2
+ Mobile bottom-sheet chrome for the dropdown panel.
3
+
4
+ These rules are keyed off plain classes rather than `:host` because both the panel
5
+ and its backdrop are portalled out to `document.body` while open. Angular's emulated
6
+ encapsulation attribute is stamped on the elements themselves and travels with them
7
+ when they are relocated, so the scoping still holds — but `:host` would no longer
8
+ match, since neither element is the host any more.
9
+
10
+ Layout (position, size, colour) lives in the template's Tailwind classes; only the
11
+ things Tailwind cannot express — keyframes and the safe-area inset — are here.
12
+ */
13
+
14
+ .mn-ms-sheet {
15
+ /* Same iOS-style decelerate curve the modal sheet uses, so the two read as one
16
+ motion language when a multi-select opens from inside a modal. */
17
+ --mn-sheet-ease: cubic-bezier(0.32, 0.72, 0, 1);
18
+
19
+ /* Keeps the last option clear of the iOS home indicator. The sheet background
20
+ extends into the inset so it still reads as one continuous surface. env() is 0
21
+ on the web, so this is a no-op outside the native shell. */
22
+ padding-bottom: env(safe-area-inset-bottom);
23
+
24
+ animation: mn-ms-sheet-in 0.3s var(--mn-sheet-ease);
25
+ }
26
+
27
+ .mn-ms-sheet-backdrop {
28
+ animation: mn-ms-backdrop-in 0.2s ease-out;
29
+ }
30
+
31
+ @keyframes mn-ms-sheet-in {
32
+ from {
33
+ transform: translateY(100%);
34
+ }
35
+ to {
36
+ transform: translateY(0);
37
+ }
38
+ }
39
+
40
+ @keyframes mn-ms-backdrop-in {
41
+ from {
42
+ opacity: 0;
43
+ }
44
+ to {
45
+ opacity: 1;
46
+ }
47
+ }
48
+
49
+ /* Collapse the entrance animations to ~instant rather than removing them, matching
50
+ the modal shell's approach so anything listening for animationend still fires. */
51
+ @media (prefers-reduced-motion: reduce) {
52
+ .mn-ms-sheet,
53
+ .mn-ms-sheet-backdrop {
54
+ animation-duration: 0.01ms !important;
55
+ }
56
+ }
@@ -2561,10 +2561,28 @@ type MnMultiSelectProps<TValue = unknown> = {
2561
2561
  placeholder?: string;
2562
2562
  /** Available options to select from */
2563
2563
  options: MnMultiSelectOption<TValue>[];
2564
- /** Whether to show a search/filter input (default: false) */
2564
+ /**
2565
+ * Whether to show a search/filter input. When omitted, search auto-enables once the
2566
+ * number of options reaches `searchThreshold`, so long lists stay filterable without
2567
+ * every call site having to opt in. Set explicitly to force it on or off.
2568
+ */
2565
2569
  searchable?: boolean;
2570
+ /**
2571
+ * Number of options at which the search input auto-enables (default: 8).
2572
+ * Ignored when `searchable` is set explicitly.
2573
+ */
2574
+ searchThreshold?: number;
2566
2575
  /** Placeholder text for the search input */
2567
2576
  searchPlaceholder?: string;
2577
+ /**
2578
+ * Whether the dropdown renders as a bottom sheet on small screens (< 640px).
2579
+ * Defaults to true. Set to false to keep the trigger-anchored panel on mobile.
2580
+ *
2581
+ * The anchored panel sits at the trigger's bottom edge, which puts it directly in
2582
+ * the path of the soft keyboard as soon as the search input takes focus. The sheet
2583
+ * is anchored to the viewport instead, so the list stays reachable.
2584
+ */
2585
+ mobileSheet?: boolean;
2568
2586
  /** Maximum number of items that can be selected (undefined = unlimited) */
2569
2587
  maxSelections?: number;
2570
2588
  /**
@@ -2614,6 +2632,8 @@ type MnMultiSelectUIConfig = {
2614
2632
  errorMessages?: Record<string, string>;
2615
2633
  /** Text shown when no options match the search filter */
2616
2634
  noOptionsFound?: string;
2635
+ /** Accessible label for the mobile sheet's close button (falls back to 'Close') */
2636
+ closeLabel?: string;
2617
2637
  };
2618
2638
 
2619
2639
  declare const MN_MULTI_SELECT_CONFIG: InjectionToken<MnMultiSelectUIConfig>;
@@ -2633,6 +2653,27 @@ declare class MnMultiSelect implements OnInit {
2633
2653
  triggerRef: ElementRef<HTMLElement>;
2634
2654
  /** The panel element currently moved into `document.body`, if any. */
2635
2655
  private movedPanel;
2656
+ /** The sheet backdrop element currently moved into `document.body`, if any. */
2657
+ private movedBackdrop;
2658
+ /** Option count at which the search input auto-enables when `searchable` is unset. */
2659
+ private static readonly DEFAULT_SEARCH_THRESHOLD;
2660
+ /** Tailwind's `sm` breakpoint — below this the panel renders as a bottom sheet.
2661
+ * Kept in step with the same constant in `MnModalShellComponent`. */
2662
+ private static readonly SHEET_MAX_WIDTH;
2663
+ /** Whether the viewport is currently narrow enough for the sheet layout. */
2664
+ private isNarrowViewport;
2665
+ /** Live breakpoint match, so rotating the device re-evaluates the layout. */
2666
+ private sheetMedia;
2667
+ /** The listener registered on `sheetMedia`, retained for teardown. */
2668
+ private sheetMediaListener;
2669
+ /** `document.body`'s inline `overflow` before the sheet locked it, restored on close. */
2670
+ private previousBodyOverflow;
2671
+ /**
2672
+ * The sheet's height (px) captured the moment it opened, before any search. Re-applied
2673
+ * as a `min-height` floor so filtering the option list shorter cannot shrink the sheet
2674
+ * mid-type. Null while anchored or closed, so the popover and desktop path are untouched.
2675
+ */
2676
+ sheetFloorPx: number | null;
2636
2677
  /**
2637
2678
  * Watches the trigger while the panel is open. The panel lives in `document.body`,
2638
2679
  * so it survives its own trigger being hidden by an ancestor — e.g. a wizard step
@@ -2655,6 +2696,12 @@ declare class MnMultiSelect implements OnInit {
2655
2696
  * broken on iOS). Cleanup is handled when the query clears on close/destroy.
2656
2697
  */
2657
2698
  set dropdownRef(ref: ElementRef<HTMLElement> | undefined);
2699
+ /**
2700
+ * The dimming backdrop rendered behind the mobile sheet. Portalled alongside the
2701
+ * panel for the same reason — a `position: fixed` backdrop left inside a transformed
2702
+ * ancestor would cover that ancestor rather than the viewport.
2703
+ */
2704
+ set sheetBackdropRef(ref: ElementRef<HTMLElement> | undefined);
2658
2705
  /** Currently selected values */
2659
2706
  selectedValues: unknown[];
2660
2707
  isOpen: boolean;
@@ -2671,6 +2718,15 @@ declare class MnMultiSelect implements OnInit {
2671
2718
  private readonly builtInErrorMessages;
2672
2719
  constructor();
2673
2720
  ngOnInit(): void;
2721
+ /**
2722
+ * Tracks the sheet breakpoint through `matchMedia` rather than reading `innerWidth`
2723
+ * once, so rotating the device switches layout instead of leaving a panel positioned
2724
+ * for the previous orientation. An open panel is closed on the switch — its anchored
2725
+ * coordinates and its sheet layout are not interchangeable.
2726
+ */
2727
+ private startWatchingViewport;
2728
+ /** Tears down the breakpoint listener. Idempotent. */
2729
+ private stopWatchingViewport;
2674
2730
  onDocumentClick(event: Event): void;
2675
2731
  private resolveConfig;
2676
2732
  writeValue(val: unknown): void;
@@ -2678,16 +2734,47 @@ declare class MnMultiSelect implements OnInit {
2678
2734
  registerOnTouched(fn: () => void): void;
2679
2735
  setDisabledState(isDisabled: boolean): void;
2680
2736
  toggle(): void;
2737
+ /** Whether the panel should currently render as a bottom sheet. */
2738
+ get isSheet(): boolean;
2739
+ /**
2740
+ * Whether the search input is shown: the explicit `searchable` prop when set,
2741
+ * otherwise auto-enabled once the option count reaches the threshold.
2742
+ */
2743
+ get isSearchable(): boolean;
2744
+ /** Layout classes for the panel — a bottom-anchored sheet, or the trigger-anchored popover. */
2745
+ get panelClasses(): string;
2746
+ /**
2747
+ * Records the sheet's opened height as its `min-height` floor. Measured on the next
2748
+ * frame so the read reflects the fully-rendered, unfiltered list (the search box is
2749
+ * empty on open) and never forces a reflow mid change-detection. The floor equals the
2750
+ * content height at that instant, so applying it triggers no resize — it only stops a
2751
+ * later, shorter filtered list from pulling the sheet down.
2752
+ */
2753
+ private captureSheetFloor;
2681
2754
  /** Closes the dropdown on Escape for keyboard accessibility. */
2682
2755
  onEscape(): void;
2683
- /** Closes the dropdown when the page or a scrollable parent is scrolled */
2756
+ /**
2757
+ * Closes the dropdown when the page or a scrollable parent is scrolled.
2758
+ *
2759
+ * Skipped for a sheet: it is anchored to the viewport, not to the trigger, so it has
2760
+ * no stale position to escape. Crucially, opening the soft keyboard fires a `resize`
2761
+ * on Android — closing on that would dismiss the sheet the instant search is focused.
2762
+ * A genuine layout switch is handled by the `matchMedia` listener instead.
2763
+ */
2684
2764
  onWindowScrollOrResize(): void;
2685
2765
  /**
2686
2766
  * The single close path. Every trigger (outside click, Escape, scroll, resize, the
2687
2767
  * trigger being hidden) funnels through here so the open-only listeners are always
2688
2768
  * torn down with the panel and never leak.
2689
2769
  */
2690
- private close;
2770
+ close(): void;
2771
+ /**
2772
+ * Freezes the page behind an open sheet. The previous inline value is captured and
2773
+ * restored verbatim so a surrounding modal that set its own lock is left intact.
2774
+ */
2775
+ private lockBodyScroll;
2776
+ /** Restores the pre-lock `overflow`. Idempotent. */
2777
+ private unlockBodyScroll;
2691
2778
  /** Calculates the fixed position for the dropdown based on the trigger element */
2692
2779
  private updateDropdownPosition;
2693
2780
  /**
@@ -2698,12 +2785,15 @@ declare class MnMultiSelect implements OnInit {
2698
2785
  */
2699
2786
  private startWatchingTrigger;
2700
2787
  /**
2701
- * Move the dropdown panel to `document.body` when it appears, and detach it when
2702
- * the query clears. Appending to the body root makes the panel immune to ancestor
2703
- * `transform`/`filter`/`will-change`, so `position: fixed` anchors to the viewport
2704
- * and the panel stays under its trigger. Idempotent and safe to call with `null`.
2788
+ * Move an overlay element to `document.body` when it appears, and detach it when the
2789
+ * query clears. Appending to the body root makes the element immune to ancestor
2790
+ * `transform`/`filter`/`will-change`, so `position: fixed` anchors to the viewport
2791
+ * without this the panel lands mid-screen (and breaks outright on iOS).
2792
+ *
2793
+ * Returns the element now portalled, so the caller can store it. Idempotent and safe
2794
+ * to call with `null`.
2705
2795
  */
2706
- private relocateDropdown;
2796
+ private portal;
2707
2797
  /** Tears down the watchers installed by `startWatchingTrigger`. Idempotent. */
2708
2798
  private stopWatchingTrigger;
2709
2799
  toggleOption(option: MnMultiSelectOption): void;