@keenmate/web-multiselect 2.0.0-rc02 → 2.0.0-rc04

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,30 @@ 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 fullscreenSearchClear;
781
+ private fullscreenNav;
782
+ private fullscreenNavCount;
783
+ private fullscreenNavPrev;
784
+ private fullscreenNavNext;
785
+ private bodyScrollUnlock;
786
+ private keyboardInsetCleanup;
787
+ private overlayHistoryActive;
788
+ private readonly onOverlayPopstate;
716
789
  private dropdownCleanup;
717
790
  private hintCleanup;
718
791
  private selectedPopoverCleanup;
719
792
  private tooltips;
793
+ private labelRevealTrigger;
794
+ private labelRevealPanel;
795
+ private labelRevealPopover;
796
+ private messageEl;
797
+ private messageCleanup;
798
+ private messageTimer;
720
799
  private readonly onDropdownScroll;
721
800
  private virtualScroll;
722
801
  private optionsContainer;
@@ -837,6 +916,23 @@ export declare class WebMultiSelect<T = any> {
837
916
  * here. Pass all options to show the whole tree.
838
917
  */
839
918
  private rebuildTreeVisibleFromMatches;
919
+ /**
920
+ * (Re)compute `isRTL` from the host's `dir` (or an RTL ancestor) and derive the
921
+ * direction-mirrored badges position. Pure state — callers apply the DOM effects
922
+ * (class toggle, panel `dir`, badge re-render). In Shadow DOM the `dir` lives on
923
+ * the host element, not the shadow content, so we resolve the host first.
924
+ */
925
+ private detectRTL;
926
+ /**
927
+ * Re-read `dir` and re-apply RTL mirroring live. The web-component calls this when
928
+ * its `dir` attribute changes at runtime (e.g. an app-wide language/direction
929
+ * switch). Most layout follows the inherited CSS `direction` automatically (the
930
+ * component is authored with logical properties); this fixes the parts pinned at
931
+ * build time — the `.ms--rtl` class (badges/count-display placement) and the
932
+ * explicit `dir` on the shadow-root-appended panels (which don't sit under the
933
+ * `.ms--rtl` element, so they'd otherwise keep a stale build-time direction).
934
+ */
935
+ refreshDirection(): void;
840
936
  private buildHTML;
841
937
  /**
842
938
  * Check if virtual scroll should be used
@@ -865,6 +961,15 @@ export declare class WebMultiSelect<T = any> {
865
961
  private getBuiltInActionDisabled;
866
962
  private renderActionsHTML;
867
963
  private renderOption;
964
+ /**
965
+ * Trailing info affordance for an option row, emitted only for the fullscreen
966
+ * overlay. CSS keeps it hidden until `markTruncatedOptions()` tags the row
967
+ * `.ms__option--truncated`, so it appears only when the label is actually clipped.
968
+ * Tapping it reveals the full label — the touch substitute for the hover option
969
+ * tooltip, which never fires on touch (the very devices that get fullscreen).
970
+ * `tabindex="-1"` keeps it out of the tab order; the search input owns keyboarding.
971
+ */
972
+ private renderOptionInfoButton;
868
973
  /**
869
974
  * Render a single tree-mode row. Separate from `renderOption`: a tree row is
870
975
  * indented by its depth (via the `--ms-tree-depth` custom property) and
@@ -974,6 +1079,114 @@ export declare class WebMultiSelect<T = any> {
974
1079
  */
975
1080
  private warnDrift;
976
1081
  private positionDropdown;
1082
+ /**
1083
+ * Switch how the open panels are presented. 'floating' anchors them to the input
1084
+ * (the default); 'fullscreen' renders them as full-viewport overlays (the phone
1085
+ * pattern) — the dropdown with its own search header + close, the selected-items
1086
+ * popover with its existing header + close. Driven by the element's
1087
+ * `environmentChanged` hook (auto → fullscreen on phones). A no-op when unchanged;
1088
+ * when a panel is already open it re-applies live so an orientation flip / viewport
1089
+ * resize can swap presentation without a reopen.
1090
+ */
1091
+ setPresentation(mode: 'floating' | 'fullscreen'): void;
1092
+ /**
1093
+ * Lock page scroll behind a fullscreen overlay via the core ref-counted helper.
1094
+ * Idempotent per instance: the dropdown and the selected-items popover are mutually
1095
+ * exclusive (opening one closes the other), so we hold at most one lock at a time,
1096
+ * and a redundant call is a no-op rather than acquiring a second.
1097
+ */
1098
+ private lockBodyScroll;
1099
+ /** Restore page scroll (no-op if it wasn't locked). */
1100
+ private unlockBodyScroll;
1101
+ /**
1102
+ * While the fullscreen dropdown is open, keep it sitting above the soft keyboard.
1103
+ * Delegates to core's `observeKeyboardInset` (which tracks `window.visualViewport`
1104
+ * and pins the panel's height/top so its flex column reflows above the keyboard);
1105
+ * we just hold the returned cleanup. No-op where `visualViewport` is unavailable.
1106
+ */
1107
+ private observeKeyboardInset;
1108
+ /** Detach keyboard-inset tracking and restore the panel's CSS-driven geometry. */
1109
+ private unobserveKeyboardInset;
1110
+ /**
1111
+ * The fullscreen size multiplier = `--ms-fullscreen-rem ÷ --ms-rem` (both read off
1112
+ * the host). CSS scales itself — every size is `calc(N × --ms-rem)` and the panel
1113
+ * overrides `--ms-rem` — so this exists only for the JS-driven pixel heights that
1114
+ * CSS can't reach: the virtual/fixed option rows and the popover's virtual badges.
1115
+ * Returns 1 when floating (or when computed styles aren't readable, e.g. jsdom).
1116
+ */
1117
+ private fullscreenScale;
1118
+ /** Virtual/fixed row height (px), scaled up in the fullscreen phone view. */
1119
+ private scaledOptionHeight;
1120
+ /**
1121
+ * Size the virtual options scroll container for the current presentation. Applied on
1122
+ * every render (the container itself is built once), so a floating⇄fullscreen switch
1123
+ * re-sizes it: floating = a fixed maxHeight scroll box; fullscreen = flex-fill the
1124
+ * panel's flex column (no fixed height). Also refreshes --ms-option-height to the
1125
+ * scaled row height so the CSS row height matches the virtual scroller's itemHeight.
1126
+ */
1127
+ private applyVirtualOptionsSizing;
1128
+ /**
1129
+ * Virtual popover badge row height (px). In the fullscreen phone view the rows are
1130
+ * scaled up AND given extra height so a selected item is a comfortable, dropdown-like
1131
+ * touch target (the default 36px pill is short for touch). Mirrors the CSS
1132
+ * `--ms-badge-height` override for the fullscreen popover (floating.css) so the
1133
+ * virtual list's fixed height agrees with the non-virtual pills.
1134
+ */
1135
+ private scaledBadgeHeight;
1136
+ /**
1137
+ * Clear the inline geometry that floating-ui's `anchor` writes on a panel
1138
+ * (position/left/top plus our composed max-width/min-width). Inline styles beat
1139
+ * the stylesheet, so a panel left over from a floating cycle would otherwise pin
1140
+ * itself where it last anchored and ignore the fullscreen CSS (position: fixed;
1141
+ * inset: 0; width: 100vw). Must run when switching a panel floating → fullscreen.
1142
+ */
1143
+ private clearFloatingInlineGeometry;
1144
+ /** Stand up the fullscreen dropdown overlay: modifier class, header, scroll lock, focus. */
1145
+ private enterFullscreen;
1146
+ /** Tear down the fullscreen dropdown chrome and restore page scroll (no-op if floating). */
1147
+ private exitFullscreen;
1148
+ /**
1149
+ * Back-gesture handling for the fullscreen sheet. On open we push a history entry
1150
+ * (same URL) and listen for `popstate`; the phone Back gesture/button then pops that
1151
+ * entry — which we treat as "close the sheet" — instead of navigating away from the
1152
+ * page. A programmatic close (✕, selection, Escape) consumes the entry via
1153
+ * `history.back()` so the stack is left as it was found.
1154
+ */
1155
+ private pushOverlayHistory;
1156
+ /** Back gesture/button fired: our pushed entry is already gone, so just close the
1157
+ * sheet — WITHOUT popping history again (popOverlayHistory becomes a no-op). */
1158
+ private handleOverlayPopstate;
1159
+ /** Programmatic close: remove the listener and pop the entry we pushed (so the
1160
+ * history stack returns to its pre-open state). No-op if a Back gesture already
1161
+ * consumed it (overlayHistoryActive is false by then). */
1162
+ private popOverlayHistory;
1163
+ /**
1164
+ * Build the fullscreen overlay header: a search field (proxying to the same
1165
+ * `handleSearch`/`handleKeydown` path as the main input, since the overlay covers
1166
+ * it) plus a close button. Inserted before the scrolling list so it pins to the
1167
+ * top of the fixed panel. `renderDropdown()` only rewrites `dropdownInner`, so the
1168
+ * header survives re-renders.
1169
+ */
1170
+ private buildFullscreenHeader;
1171
+ /**
1172
+ * Sync the fullscreen match navigator (navigate mode only) with the current search
1173
+ * state: hide it until there's a term, then show "N of M" while a match is focused
1174
+ * (or "M matches" / "No matches"), and disable the prev/next buttons when there's
1175
+ * nothing to step through. No-op when the navigator isn't built (floating panel,
1176
+ * filter mode, or search disabled).
1177
+ */
1178
+ private updateFullscreenNav;
1179
+ /**
1180
+ * Show the fullscreen search's inline clear (✕) only while the field has text.
1181
+ * No-op when the button isn't built (floating panel, readonly/hidden search).
1182
+ */
1183
+ private updateFullscreenSearchClear;
1184
+ /**
1185
+ * Clear the fullscreen search term via the same path a keystroke takes, then
1186
+ * refocus the field so the user can keep typing. Touch has no keyboard Escape,
1187
+ * so this button is the on-screen way to reset a search.
1188
+ */
1189
+ private clearFullscreenSearch;
977
1190
  private positionHint;
978
1191
  private parseInitialSelection;
979
1192
  /**
@@ -1052,6 +1265,42 @@ export declare class WebMultiSelect<T = any> {
1052
1265
  * `filteredOptions`, the same global index `renderOption` was given.
1053
1266
  */
1054
1267
  private attachOptionTooltips;
1268
+ /**
1269
+ * Tag each currently-rendered fullscreen option row whose title is horizontally
1270
+ * clipped with `.ms__option--truncated`, so CSS reveals its info affordance.
1271
+ * Runs per virtual-scroll render (rows recycle) and on the non-virtual render.
1272
+ * Horizontal (ellipsis) overflow only — the truncation mode this pairs with;
1273
+ * a wrapping title isn't "cut", it grows vertically. No-op unless fullscreen.
1274
+ */
1275
+ private markTruncatedOptions;
1276
+ /**
1277
+ * Reveal (or dismiss) the full label of a clipped fullscreen row when its info
1278
+ * affordance is tapped — hover tooltips don't fire on touch, and a hover tooltip's
1279
+ * synthetic mouseleave (from the tap itself, under devtools touch emulation) would
1280
+ * flash it away. So this is a manually-controlled `createPopover` panel, mounted in
1281
+ * the shadow root for component styling, that stays until explicitly dismissed:
1282
+ * a second tap on the same button, a list scroll, a re-render, an outside tap, or
1283
+ * closing the panel (see hideLabelReveal + its call sites). Tapping the same button
1284
+ * while it's shown toggles it off.
1285
+ */
1286
+ private toggleLabelReveal;
1287
+ /** Dismiss the full-label reveal popover, if shown. Idempotent. */
1288
+ private hideLabelReveal;
1289
+ /**
1290
+ * Show a transient message ("toast") on top of the component. Its reason for existing:
1291
+ * in the fullscreen overlay the sheet covers the whole page, so a consumer can't surface
1292
+ * feedback (a blocked veto, a hint) where the user can see it. This renders above the
1293
+ * panel in BOTH presentations — anchored under the control when floating, pinned to the
1294
+ * bottom of the viewport (over the overlay) when fullscreen.
1295
+ *
1296
+ * Content is a string (plain text) or an HTMLElement (rich markup). `opts.variant`
1297
+ * (info | warning | error | success) picks the tone; `opts.duration` sets auto-dismiss
1298
+ * (0 = sticky). Tapping the message dismisses it. Only one shows at a time — a new call
1299
+ * replaces the previous. Also reached automatically when a veto callback returns a string.
1300
+ */
1301
+ showMessage(content: string | HTMLElement, opts?: MessageOptions): void;
1302
+ /** Dismiss the transient message, if shown. Idempotent. */
1303
+ hideMessage(): void;
1055
1304
  /**
1056
1305
  * Hide (don't destroy) every currently-shown option tooltip immediately,
1057
1306
  * ignoring the hide delay. Wired to dropdown scroll so a tooltip can't trail