@keenmate/web-multiselect 2.0.0-rc01 → 2.0.0-rc03

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/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { BlissElement } from '@keenmate/web-components-core';
2
+ import { EnvironmentSnapshot } from '@keenmate/web-components-core';
2
3
  import { InputDef } from '@keenmate/web-components-core';
3
4
  import { Logger } from '@keenmate/web-components-core';
4
5
  import { LogLevelDesc } from '@keenmate/web-components-core';
@@ -123,6 +124,31 @@ declare interface LTreeNode<T> {
123
124
  data: T | null | undefined;
124
125
  }
125
126
 
127
+ /**
128
+ * Options for `showMessage()` — the transient toast the component can surface on top of
129
+ * itself. It exists mainly so a veto (or any consumer feedback) is visible in the
130
+ * fullscreen overlay, where page-level UI is covered by the sheet.
131
+ */
132
+ declare interface MessageOptions {
133
+ /** Visual tone. Default `'info'`. */
134
+ variant?: MessageVariant;
135
+ /** Auto-dismiss after this many ms. `0` keeps it until replaced, tapped, or the panel closes. Default `3000`. */
136
+ duration?: number;
137
+ /**
138
+ * Where the message anchors relative to the control in the **floating/anchored** case
139
+ * (a floating-ui `Placement`, e.g. `'top'`, `'bottom-start'`, `'right'`). Default
140
+ * `'bottom'`. Ignored when a fullscreen overlay is open — there the message is pinned
141
+ * to the bottom-centre of the viewport.
142
+ */
143
+ placement?: Placement;
144
+ }
145
+
146
+ /**
147
+ * Visual tone of a transient message shown via `showMessage()` (or a veto callback's
148
+ * returned reason string). Each maps to a `.ms__message--{variant}` theming hook.
149
+ */
150
+ declare type MessageVariant = 'info' | 'warning' | 'error' | 'success';
151
+
126
152
  /**
127
153
  * Generic configuration options for the MultiSelect component
128
154
  * @template T The type of data items
@@ -261,6 +287,13 @@ declare interface MultiSelectConfig<T = any> {
261
287
  isActionsSticky?: boolean;
262
288
  /** Close dropdown after selecting an option (internal: isCloseOnSelect) */
263
289
  isCloseOnSelect?: boolean;
290
+ /**
291
+ * In the phone fullscreen overlay, auto-focus the search field when it opens — which
292
+ * pops the soft keyboard immediately. Default `false`: the sheet opens showing the list
293
+ * (keyboard closed), and the keyboard appears only when the user taps the search. Set
294
+ * `true` to type-to-filter right away (matches native pickers). No effect in the floating
295
+ * presentation. (internal: fullscreenAutofocus) */
296
+ fullscreenAutofocus?: boolean;
264
297
  /** Lock dropdown placement after first open (internal: isPlacementLocked) */
265
298
  isPlacementLocked?: boolean;
266
299
  /** Allow adding new options not in the list (internal: isAddNewAllowed) */
@@ -404,20 +437,23 @@ declare interface MultiSelectConfig<T = any> {
404
437
  /**
405
438
  * Interceptor: runs before an option is selected via user interaction.
406
439
  * Receives the option about to be added and the current selection (before the change).
407
- * Return `false` to block the selection; return `true`/`undefined` to allow.
408
- * Silent a blocked action fires no event. Bypassed by programmatic `setSelected`
409
- * and the Select-All action button.
440
+ * Return `false` to block the selection; return `true`/`undefined` to allow. Return a
441
+ * **string** to block AND surface it as a message (see `showMessage`) — the touch-safe
442
+ * way to explain a veto in the fullscreen overlay, where page-level UI is hidden behind
443
+ * it. Silent otherwise — a blocked action fires no event. Bypassed by programmatic
444
+ * `setSelected` and the Select-All action button.
410
445
  */
411
- beforeSelectCallback?: ((option: T, selectedOptions: T[]) => boolean | void) | null;
446
+ beforeSelectCallback?: ((option: T, selectedOptions: T[]) => boolean | string | void) | null;
412
447
  /**
413
448
  * Interceptor: runs before an option is deselected via user interaction — the dropdown
414
449
  * option toggle, a badge's remove (×) button, the selected-items popover's remove button,
415
450
  * and the "remove hidden" badge (checked per item). Receives the option about to be
416
451
  * removed and the current selection (before the change). Return `false` to block the
417
- * deselection; return `true`/`undefined` to allow. Silent a blocked action fires no
418
- * event. Bypassed by programmatic `setSelected` and the Clear-All action button.
452
+ * deselection; return `true`/`undefined` to allow. Return a **string** to block AND
453
+ * surface it as a message (see `showMessage`). Silent otherwise a blocked action fires
454
+ * no event. Bypassed by programmatic `setSelected` and the Clear-All action button.
419
455
  */
420
- beforeDeselectCallback?: ((option: T, selectedOptions: T[]) => boolean | void) | null;
456
+ beforeDeselectCallback?: ((option: T, selectedOptions: T[]) => boolean | string | void) | null;
421
457
  /**
422
458
  * Async function to load data: `(searchTerm, signal) => Promise<options[]>`.
423
459
  * The optional second argument is an `AbortSignal` that fires when a newer search
@@ -502,6 +538,11 @@ export declare class MultiSelectElement<T = any> extends BlissElement<MultiSelec
502
538
  * picker's selection so the control participates in the standard reset.
503
539
  */
504
540
  formResetCallback(): void;
541
+ /** Runtime writing-direction switch (core observes `dir`): re-mirror the live
542
+ * picker. Layout mostly follows the inherited `direction` (logical properties);
543
+ * refreshDirection() fixes the parts pinned at build time (the `.ms--rtl` class
544
+ * and the panels' explicit `dir`). The initial direction is read by the build. */
545
+ protected directionChanged(_isRTL: boolean): void;
505
546
  /** Structural change (or first connect): mirror CSS vars, then (re)build the picker. */
506
547
  protected reinit(): void;
507
548
  /** Cosmetic change: mirror CSS vars / custom styles / debug, patch the picker in place. */
@@ -510,6 +551,15 @@ export declare class MultiSelectElement<T = any> extends BlissElement<MultiSelec
510
551
  protected connect(): void;
511
552
  /** Deactivate: tear the picker down (rebuilt on the next connect). */
512
553
  protected disconnect(): void;
554
+ /**
555
+ * Device/viewport/orientation changed (core §12.9). Overriding this opts the
556
+ * element into the shared environment observable — core subscribes on connect
557
+ * (firing immediately with the current snapshot) and unsubscribes on disconnect.
558
+ * We map it to the picker's floating/fullscreen presentation; the immediate fire
559
+ * lands right after `connect()` builds the picker, so the initial presentation is
560
+ * set before the dropdown can open.
561
+ */
562
+ protected environmentChanged(env: EnvironmentSnapshot): void;
513
563
  /** Form field name (mirrors the `name` attribute → `formFieldId`). */
514
564
  get name(): string | null;
515
565
  set name(value: string | null);
@@ -520,6 +570,16 @@ export declare class MultiSelectElement<T = any> extends BlissElement<MultiSelec
520
570
  notify?: boolean;
521
571
  }): void;
522
572
  getValue(): string | number | (string | number)[] | null;
573
+ /**
574
+ * Surface a transient message ("toast") on top of the component — visible even in the
575
+ * fullscreen overlay, where page-level UI is hidden behind the sheet. Content is text or
576
+ * an element; `opts.variant` sets the tone and `opts.duration` the auto-dismiss (0 =
577
+ * sticky). Also reached automatically when a `beforeSelect`/`beforeDeselect` callback
578
+ * returns a reason string.
579
+ */
580
+ showMessage(content: string | HTMLElement, opts?: MessageOptions): void;
581
+ /** Dismiss the transient message shown by {@link showMessage}, if any. */
582
+ hideMessage(): void;
523
583
  destroy(): void;
524
584
  }
525
585
 
@@ -712,11 +772,29 @@ export declare class WebMultiSelect<T = any> {
712
772
  private isRTL;
713
773
  private effectiveBadgesPosition;
714
774
  private justClosedViaClick;
775
+ private justOpenedViaClick;
715
776
  private positioningDriftWarned;
777
+ private presentationMode;
778
+ private fullscreenHeader;
779
+ private fullscreenSearchInput;
780
+ private fullscreenNav;
781
+ private fullscreenNavCount;
782
+ private fullscreenNavPrev;
783
+ private fullscreenNavNext;
784
+ private bodyScrollUnlock;
785
+ private keyboardInsetCleanup;
786
+ private overlayHistoryActive;
787
+ private readonly onOverlayPopstate;
716
788
  private dropdownCleanup;
717
789
  private hintCleanup;
718
790
  private selectedPopoverCleanup;
719
791
  private tooltips;
792
+ private labelRevealTrigger;
793
+ private labelRevealPanel;
794
+ private labelRevealPopover;
795
+ private messageEl;
796
+ private messageCleanup;
797
+ private messageTimer;
720
798
  private readonly onDropdownScroll;
721
799
  private virtualScroll;
722
800
  private optionsContainer;
@@ -837,6 +915,23 @@ export declare class WebMultiSelect<T = any> {
837
915
  * here. Pass all options to show the whole tree.
838
916
  */
839
917
  private rebuildTreeVisibleFromMatches;
918
+ /**
919
+ * (Re)compute `isRTL` from the host's `dir` (or an RTL ancestor) and derive the
920
+ * direction-mirrored badges position. Pure state — callers apply the DOM effects
921
+ * (class toggle, panel `dir`, badge re-render). In Shadow DOM the `dir` lives on
922
+ * the host element, not the shadow content, so we resolve the host first.
923
+ */
924
+ private detectRTL;
925
+ /**
926
+ * Re-read `dir` and re-apply RTL mirroring live. The web-component calls this when
927
+ * its `dir` attribute changes at runtime (e.g. an app-wide language/direction
928
+ * switch). Most layout follows the inherited CSS `direction` automatically (the
929
+ * component is authored with logical properties); this fixes the parts pinned at
930
+ * build time — the `.ms--rtl` class (badges/count-display placement) and the
931
+ * explicit `dir` on the shadow-root-appended panels (which don't sit under the
932
+ * `.ms--rtl` element, so they'd otherwise keep a stale build-time direction).
933
+ */
934
+ refreshDirection(): void;
840
935
  private buildHTML;
841
936
  /**
842
937
  * Check if virtual scroll should be used
@@ -865,6 +960,15 @@ export declare class WebMultiSelect<T = any> {
865
960
  private getBuiltInActionDisabled;
866
961
  private renderActionsHTML;
867
962
  private renderOption;
963
+ /**
964
+ * Trailing info affordance for an option row, emitted only for the fullscreen
965
+ * overlay. CSS keeps it hidden until `markTruncatedOptions()` tags the row
966
+ * `.ms__option--truncated`, so it appears only when the label is actually clipped.
967
+ * Tapping it reveals the full label — the touch substitute for the hover option
968
+ * tooltip, which never fires on touch (the very devices that get fullscreen).
969
+ * `tabindex="-1"` keeps it out of the tab order; the search input owns keyboarding.
970
+ */
971
+ private renderOptionInfoButton;
868
972
  /**
869
973
  * Render a single tree-mode row. Separate from `renderOption`: a tree row is
870
974
  * indented by its depth (via the `--ms-tree-depth` custom property) and
@@ -974,6 +1078,103 @@ export declare class WebMultiSelect<T = any> {
974
1078
  */
975
1079
  private warnDrift;
976
1080
  private positionDropdown;
1081
+ /**
1082
+ * Switch how the open panels are presented. 'floating' anchors them to the input
1083
+ * (the default); 'fullscreen' renders them as full-viewport overlays (the phone
1084
+ * pattern) — the dropdown with its own search header + close, the selected-items
1085
+ * popover with its existing header + close. Driven by the element's
1086
+ * `environmentChanged` hook (auto → fullscreen on phones). A no-op when unchanged;
1087
+ * when a panel is already open it re-applies live so an orientation flip / viewport
1088
+ * resize can swap presentation without a reopen.
1089
+ */
1090
+ setPresentation(mode: 'floating' | 'fullscreen'): void;
1091
+ /**
1092
+ * Lock page scroll behind a fullscreen overlay via the core ref-counted helper.
1093
+ * Idempotent per instance: the dropdown and the selected-items popover are mutually
1094
+ * exclusive (opening one closes the other), so we hold at most one lock at a time,
1095
+ * and a redundant call is a no-op rather than acquiring a second.
1096
+ */
1097
+ private lockBodyScroll;
1098
+ /** Restore page scroll (no-op if it wasn't locked). */
1099
+ private unlockBodyScroll;
1100
+ /**
1101
+ * While the fullscreen dropdown is open, keep it sitting above the soft keyboard.
1102
+ * Delegates to core's `observeKeyboardInset` (which tracks `window.visualViewport`
1103
+ * and pins the panel's height/top so its flex column reflows above the keyboard);
1104
+ * we just hold the returned cleanup. No-op where `visualViewport` is unavailable.
1105
+ */
1106
+ private observeKeyboardInset;
1107
+ /** Detach keyboard-inset tracking and restore the panel's CSS-driven geometry. */
1108
+ private unobserveKeyboardInset;
1109
+ /**
1110
+ * The fullscreen size multiplier = `--ms-fullscreen-rem ÷ --ms-rem` (both read off
1111
+ * the host). CSS scales itself — every size is `calc(N × --ms-rem)` and the panel
1112
+ * overrides `--ms-rem` — so this exists only for the JS-driven pixel heights that
1113
+ * CSS can't reach: the virtual/fixed option rows and the popover's virtual badges.
1114
+ * Returns 1 when floating (or when computed styles aren't readable, e.g. jsdom).
1115
+ */
1116
+ private fullscreenScale;
1117
+ /** Virtual/fixed row height (px), scaled up in the fullscreen phone view. */
1118
+ private scaledOptionHeight;
1119
+ /**
1120
+ * Size the virtual options scroll container for the current presentation. Applied on
1121
+ * every render (the container itself is built once), so a floating⇄fullscreen switch
1122
+ * re-sizes it: floating = a fixed maxHeight scroll box; fullscreen = flex-fill the
1123
+ * panel's flex column (no fixed height). Also refreshes --ms-option-height to the
1124
+ * scaled row height so the CSS row height matches the virtual scroller's itemHeight.
1125
+ */
1126
+ private applyVirtualOptionsSizing;
1127
+ /**
1128
+ * Virtual popover badge row height (px). In the fullscreen phone view the rows are
1129
+ * scaled up AND given extra height so a selected item is a comfortable, dropdown-like
1130
+ * touch target (the default 36px pill is short for touch). Mirrors the CSS
1131
+ * `--ms-badge-height` override for the fullscreen popover (floating.css) so the
1132
+ * virtual list's fixed height agrees with the non-virtual pills.
1133
+ */
1134
+ private scaledBadgeHeight;
1135
+ /**
1136
+ * Clear the inline geometry that floating-ui's `anchor` writes on a panel
1137
+ * (position/left/top plus our composed max-width/min-width). Inline styles beat
1138
+ * the stylesheet, so a panel left over from a floating cycle would otherwise pin
1139
+ * itself where it last anchored and ignore the fullscreen CSS (position: fixed;
1140
+ * inset: 0; width: 100vw). Must run when switching a panel floating → fullscreen.
1141
+ */
1142
+ private clearFloatingInlineGeometry;
1143
+ /** Stand up the fullscreen dropdown overlay: modifier class, header, scroll lock, focus. */
1144
+ private enterFullscreen;
1145
+ /** Tear down the fullscreen dropdown chrome and restore page scroll (no-op if floating). */
1146
+ private exitFullscreen;
1147
+ /**
1148
+ * Back-gesture handling for the fullscreen sheet. On open we push a history entry
1149
+ * (same URL) and listen for `popstate`; the phone Back gesture/button then pops that
1150
+ * entry — which we treat as "close the sheet" — instead of navigating away from the
1151
+ * page. A programmatic close (✕, selection, Escape) consumes the entry via
1152
+ * `history.back()` so the stack is left as it was found.
1153
+ */
1154
+ private pushOverlayHistory;
1155
+ /** Back gesture/button fired: our pushed entry is already gone, so just close the
1156
+ * sheet — WITHOUT popping history again (popOverlayHistory becomes a no-op). */
1157
+ private handleOverlayPopstate;
1158
+ /** Programmatic close: remove the listener and pop the entry we pushed (so the
1159
+ * history stack returns to its pre-open state). No-op if a Back gesture already
1160
+ * consumed it (overlayHistoryActive is false by then). */
1161
+ private popOverlayHistory;
1162
+ /**
1163
+ * Build the fullscreen overlay header: a search field (proxying to the same
1164
+ * `handleSearch`/`handleKeydown` path as the main input, since the overlay covers
1165
+ * it) plus a close button. Inserted before the scrolling list so it pins to the
1166
+ * top of the fixed panel. `renderDropdown()` only rewrites `dropdownInner`, so the
1167
+ * header survives re-renders.
1168
+ */
1169
+ private buildFullscreenHeader;
1170
+ /**
1171
+ * Sync the fullscreen match navigator (navigate mode only) with the current search
1172
+ * state: hide it until there's a term, then show "N of M" while a match is focused
1173
+ * (or "M matches" / "No matches"), and disable the prev/next buttons when there's
1174
+ * nothing to step through. No-op when the navigator isn't built (floating panel,
1175
+ * filter mode, or search disabled).
1176
+ */
1177
+ private updateFullscreenNav;
977
1178
  private positionHint;
978
1179
  private parseInitialSelection;
979
1180
  /**
@@ -1052,6 +1253,42 @@ export declare class WebMultiSelect<T = any> {
1052
1253
  * `filteredOptions`, the same global index `renderOption` was given.
1053
1254
  */
1054
1255
  private attachOptionTooltips;
1256
+ /**
1257
+ * Tag each currently-rendered fullscreen option row whose title is horizontally
1258
+ * clipped with `.ms__option--truncated`, so CSS reveals its info affordance.
1259
+ * Runs per virtual-scroll render (rows recycle) and on the non-virtual render.
1260
+ * Horizontal (ellipsis) overflow only — the truncation mode this pairs with;
1261
+ * a wrapping title isn't "cut", it grows vertically. No-op unless fullscreen.
1262
+ */
1263
+ private markTruncatedOptions;
1264
+ /**
1265
+ * Reveal (or dismiss) the full label of a clipped fullscreen row when its info
1266
+ * affordance is tapped — hover tooltips don't fire on touch, and a hover tooltip's
1267
+ * synthetic mouseleave (from the tap itself, under devtools touch emulation) would
1268
+ * flash it away. So this is a manually-controlled `createPopover` panel, mounted in
1269
+ * the shadow root for component styling, that stays until explicitly dismissed:
1270
+ * a second tap on the same button, a list scroll, a re-render, an outside tap, or
1271
+ * closing the panel (see hideLabelReveal + its call sites). Tapping the same button
1272
+ * while it's shown toggles it off.
1273
+ */
1274
+ private toggleLabelReveal;
1275
+ /** Dismiss the full-label reveal popover, if shown. Idempotent. */
1276
+ private hideLabelReveal;
1277
+ /**
1278
+ * Show a transient message ("toast") on top of the component. Its reason for existing:
1279
+ * in the fullscreen overlay the sheet covers the whole page, so a consumer can't surface
1280
+ * feedback (a blocked veto, a hint) where the user can see it. This renders above the
1281
+ * panel in BOTH presentations — anchored under the control when floating, pinned to the
1282
+ * bottom of the viewport (over the overlay) when fullscreen.
1283
+ *
1284
+ * Content is a string (plain text) or an HTMLElement (rich markup). `opts.variant`
1285
+ * (info | warning | error | success) picks the tone; `opts.duration` sets auto-dismiss
1286
+ * (0 = sticky). Tapping the message dismisses it. Only one shows at a time — a new call
1287
+ * replaces the previous. Also reached automatically when a veto callback returns a string.
1288
+ */
1289
+ showMessage(content: string | HTMLElement, opts?: MessageOptions): void;
1290
+ /** Dismiss the transient message, if shown. Idempotent. */
1291
+ hideMessage(): void;
1055
1292
  /**
1056
1293
  * Hide (don't destroy) every currently-shown option tooltip immediately,
1057
1294
  * ignoring the hide delay. Wired to dropdown scroll so a tooltip can't trail