@keenmate/web-multiselect 2.0.0-rc06 → 2.0.0-rc08

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
@@ -4,6 +4,7 @@ import { InputDef } from '@keenmate/web-components-core';
4
4
  import { Logger } from '@keenmate/web-components-core';
5
5
  import { LogLevelDesc } from '@keenmate/web-components-core';
6
6
  import { Placement } from '@keenmate/web-components-core/positioning';
7
+ import { PresentationContext } from '@keenmate/web-components-core';
7
8
 
8
9
  /**
9
10
  * Action button configuration for dropdown actions (Select All, Clear All, custom actions)
@@ -59,7 +60,7 @@ declare type ActionsPosition = 'top' | 'bottom';
59
60
  /**
60
61
  * Context provided to renderBadgeContentCallback
61
62
  */
62
- declare interface BadgeContentRenderContext {
63
+ declare interface BadgeContentRenderContext extends PresentationContext {
63
64
  /** Current badges display mode */
64
65
  displayMode: BadgesDisplayMode;
65
66
  /** Whether the badge is being rendered in the selected items popover */
@@ -387,6 +388,17 @@ declare interface MultiSelectConfig<T = any> {
387
388
  * - `navigate` — keep all options visible and jump focus to matches
388
389
  */
389
390
  searchMode?: SearchMode;
391
+ /**
392
+ * Show a clickable mode toggle in the phone fullscreen overlay's search header that
393
+ * flips `searchMode` between `filter` and `navigate` live (no reopen). Default `false`.
394
+ *
395
+ * The overlay has room for the affordance and touch users can't reach the desktop
396
+ * `Ctrl`+`Arrow` match-stepping, so this exposes both modes on the device where it
397
+ * matters most. The toggle sits at the leading edge of the search field; its icon
398
+ * reflects the current mode (magnifier = navigate, funnel = filter). No effect in the
399
+ * floating presentation or when search is disabled/hidden.
400
+ */
401
+ isSearchModeToggleShown?: boolean;
390
402
  /**
391
403
  * Layout mode for the action buttons. Default: `nowrap`.
392
404
  *
@@ -463,6 +475,15 @@ declare interface MultiSelectConfig<T = any> {
463
475
  searchCallback?: ((searchTerm: string, signal?: AbortSignal) => Promise<T[]>) | null;
464
476
  /** Callback to add a new option when isAddNewAllowed is true */
465
477
  addNewCallback?: ((value: string) => T | Promise<T>) | null;
478
+ /**
479
+ * Intercept keyboard input before the built-in handling. Runs on every keydown (open or
480
+ * closed) with a {@link MultiSelectKeydownContext} carrying the event, current state, and a
481
+ * {@link MultiSelectKeyboardController}. Return `true` to mark the key fully handled — the
482
+ * component then runs none of its own key logic (you own `preventDefault`); return
483
+ * `false`/`undefined` to fall through to the defaults. Use it to remap keys (Vim `j`/`k`),
484
+ * add shortcuts (Ctrl+A → select all), or suppress a default. Property-only.
485
+ */
486
+ keydownCallback?: ((context: MultiSelectKeydownContext<T>) => boolean | void) | null;
466
487
  /** Event handler: an option was selected (fire-and-forget; return value ignored). Mirrors the bubbling `select` CustomEvent on the element. */
467
488
  onSelect?: ((option: T) => void) | null;
468
489
  /** Event handler: an option was deselected (fire-and-forget). Mirrors the bubbling `deselect` CustomEvent on the element. */
@@ -602,6 +623,71 @@ declare type MultiSelectEvents = {
602
623
  change: MultiSelectEventDetail;
603
624
  };
604
625
 
626
+ /**
627
+ * Imperative facade handed to `keydownCallback` so a consumer can drive the picker without
628
+ * reaching into internals. Every method mirrors a built-in keyboard action.
629
+ */
630
+ declare interface MultiSelectKeyboardController<T = any> {
631
+ /** Move focus to the next / previous option. */
632
+ focusNext(): void;
633
+ focusPrevious(): void;
634
+ /** Move focus to the first / last option. */
635
+ focusFirst(): void;
636
+ focusLast(): void;
637
+ /** Move focus by a page (10 rows). */
638
+ focusPageUp(): void;
639
+ focusPageDown(): void;
640
+ /** Navigate-mode only: jump focus to the next / previous match. */
641
+ focusNextMatch(): void;
642
+ focusPreviousMatch(): void;
643
+ /** Focus a specific index in the filtered list (ignored if out of range). */
644
+ focusIndex(index: number): void;
645
+ /** Toggle the currently focused option (no-op if nothing is focused). */
646
+ toggleFocused(): void;
647
+ /** Toggle a specific option by its value (select ⇄ deselect). */
648
+ toggleValue(value: string | number): void;
649
+ /** Select a specific option by its value (no-op if already selected). */
650
+ selectValue(value: string | number): void;
651
+ /** Deselect a specific option by its value (no-op if not selected). */
652
+ deselectValue(value: string | number): void;
653
+ /** Open / close the dropdown. */
654
+ open(): void;
655
+ close(): void;
656
+ /** Set the search box text (runs the search). */
657
+ setSearch(term: string): void;
658
+ /** Clear the search box and reset the visible list. */
659
+ clearSearch(): void;
660
+ }
661
+
662
+ /**
663
+ * Context passed to `keydownCallback`, which runs before all built-in keyboard handling.
664
+ * Return `true` to mark the key fully handled (the component then runs none of its own
665
+ * key logic — you own `preventDefault`); return `false`/`undefined` to fall through to the
666
+ * defaults. Mirrors the veto-hook shape used across KM components.
667
+ */
668
+ declare interface MultiSelectKeydownContext<T = any> {
669
+ /** The raw keyboard event — call `preventDefault()` yourself if you handle the key. */
670
+ event: KeyboardEvent;
671
+ /** `event.key`, for convenience. */
672
+ key: string;
673
+ /** Whether the dropdown is currently open. */
674
+ isOpen: boolean;
675
+ /** How the open panel is presented (`floating` / `fullscreen`). */
676
+ presentation: 'floating' | 'fullscreen';
677
+ /** The current search term. */
678
+ searchTerm: string;
679
+ /** Index of the focused option in the filtered list (`-1` when nothing is focused). */
680
+ focusedIndex: number;
681
+ /** The focused option object, or `null`. */
682
+ focusedOption: T | null;
683
+ /** The options currently visible (after filtering). */
684
+ filteredOptions: ReadonlyArray<T>;
685
+ /** The currently selected values. */
686
+ selectedValues: ReadonlyArray<string>;
687
+ /** Imperative actions mirroring the built-in keyboard behavior. */
688
+ controller: MultiSelectKeyboardController<T>;
689
+ }
690
+
605
691
  /**
606
692
  * Legacy interface for backward reference
607
693
  * Note: New code should use generic types with member/callback properties
@@ -647,9 +733,15 @@ export declare interface MultiSelectOptions extends MultiSelectConfig<MultiSelec
647
733
  declare type NodeId = string | number;
648
734
 
649
735
  /**
650
- * Context provided to renderOptionContentCallback
736
+ * Context provided to renderOptionContentCallback.
737
+ *
738
+ * Extends the shared {@link PresentationContext} from `@keenmate/web-components-core`, so it
739
+ * also carries `presentation` (`'floating' | 'modal' | 'fullscreen'` — this component only ever
740
+ * emits `floating`/`fullscreen`), `isFullscreen`, and `isModal`. Branch on `isFullscreen` to
741
+ * render leaner content in the phone overlay. Reactive: swapping presentation re-renders and
742
+ * re-invokes the callback with the new value.
651
743
  */
652
- declare interface OptionContentRenderContext {
744
+ declare interface OptionContentRenderContext extends PresentationContext {
653
745
  /** Index of the option in the filtered list */
654
746
  index: number;
655
747
  /** Whether the option is currently selected */
@@ -761,6 +853,7 @@ export declare class WebMultiSelect<T = any> {
761
853
  private cascadeCheckedAtoms;
762
854
  private hiddenInputs;
763
855
  private focusedIndex;
856
+ private keyboardController;
764
857
  private matchingIndices;
765
858
  private searchTerm;
766
859
  private isLoading;
@@ -778,6 +871,7 @@ export declare class WebMultiSelect<T = any> {
778
871
  private fullscreenHeader;
779
872
  private fullscreenSearchInput;
780
873
  private fullscreenSearchClear;
874
+ private fullscreenModeToggle;
781
875
  private fullscreenNav;
782
876
  private fullscreenNavCount;
783
877
  private fullscreenNavPrev;
@@ -999,6 +1093,16 @@ export declare class WebMultiSelect<T = any> {
999
1093
  * search is unusable → the search placeholder.
1000
1094
  */
1001
1095
  private getPlaceholderText;
1096
+ /**
1097
+ * The search field placeholder. An explicit `searchPlaceholder` always wins and stays
1098
+ * fixed. Otherwise the default is "Search..." — except when the in-overlay mode toggle
1099
+ * is enabled (`isSearchModeToggleShown`), where it becomes mode-aware so the field labels
1100
+ * the current behavior: "Search…" in navigate mode, "Filter…" in filter mode. Refreshed
1101
+ * on a live mode switch (see setSearchModeLive → refreshSearchPlaceholder).
1102
+ */
1103
+ private getSearchPlaceholder;
1104
+ /** Re-apply the (possibly mode-aware) placeholder to the live inputs after a mode switch. */
1105
+ private refreshSearchPlaceholder;
1002
1106
  private renderBadges;
1003
1107
  private attachEvents;
1004
1108
  private handleSearch;
@@ -1040,6 +1144,12 @@ export declare class WebMultiSelect<T = any> {
1040
1144
  private focusPageDown;
1041
1145
  private focusNextMatch;
1042
1146
  private focusPreviousMatch;
1147
+ /** Lazily build (and cache) the imperative facade passed to `keydownCallback`. Bound to the
1148
+ * same private actions the built-in key handling uses, so consumer shortcuts behave identically. */
1149
+ private getKeyboardController;
1150
+ /** Clear the search box (both the main input and the fullscreen search) and reset the visible
1151
+ * list. Shared by Escape and the keyboard controller. */
1152
+ private clearSearch;
1043
1153
  private scrollToFocused;
1044
1154
  private toggleOption;
1045
1155
  /**
@@ -1199,6 +1309,26 @@ export declare class WebMultiSelect<T = any> {
1199
1309
  * header survives re-renders.
1200
1310
  */
1201
1311
  private buildFullscreenHeader;
1312
+ /** Build the navigate-mode match navigator (count + prev/next) and append it to the
1313
+ * fullscreen header, once. No-op if already built or the header isn't present. The
1314
+ * nav wraps onto its own full-width row under the search box (header is flex-wrap;
1315
+ * the nav takes 100% basis). */
1316
+ private ensureFullscreenNav;
1317
+ /** Remove the match navigator (switching to filter mode, which has no jump UI). */
1318
+ private removeFullscreenNav;
1319
+ /** Flip searchMode filter<->navigate from the in-overlay toggle. */
1320
+ private toggleSearchModeLive;
1321
+ /**
1322
+ * Switch searchMode in place — the overlay's toggle path. The `search-mode` attribute
1323
+ * is reinit-on-change (it rebuilds and closes the overlay); this instead mutates the
1324
+ * live config, adds/removes the match navigator to match, and re-projects the current
1325
+ * term under the new mode (filter narrows the list / navigate keeps all + highlights),
1326
+ * all without tearing the open sheet down. Focus stays on the search field.
1327
+ */
1328
+ private setSearchModeLive;
1329
+ /** Sync the mode toggle's icon (via data-mode) and labels with the current searchMode.
1330
+ * No-op when the toggle isn't built (opt-out, floating panel, or search hidden). */
1331
+ private updateFullscreenModeToggle;
1202
1332
  /**
1203
1333
  * Sync the fullscreen match navigator (navigate mode only) with the current search
1204
1334
  * state: hide it until there's a term, then show "N of M" while a match is focused