@keenmate/web-multiselect 2.1.0 → 2.2.0-rc02

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
@@ -72,7 +72,7 @@ declare type ActionsPosition = 'top' | 'bottom';
72
72
  /**
73
73
  * Context provided to renderBadgeContentCallback
74
74
  */
75
- declare interface BadgeContentRenderContext extends PresentationContext {
75
+ export declare interface BadgeContentRenderContext extends PresentationContext {
76
76
  /** Current badges display mode */
77
77
  displayMode: BadgesDisplayMode;
78
78
  /** Whether the badge is being rendered in the selected items popover */
@@ -114,6 +114,35 @@ export { EnvironmentSnapshot }
114
114
 
115
115
  export { getEnvironment }
116
116
 
117
+ /**
118
+ * Context handed to `renderGroupLabelContentCallback` as its second argument, so a custom group
119
+ * header can reflect the selection — e.g. render a "3 / 8" count next to the title. The fields are
120
+ * populated for every group header; the selection fields are meaningful mainly under
121
+ * `groupSelectMode: 'cascade'` (a flat multi-select grouped list). Under the default rendering
122
+ * (no callback) the component draws the count itself; with a callback, YOU own the content and can
123
+ * render the count however you like from these fields.
124
+ *
125
+ * Like {@link OptionContentRenderContext} / {@link BadgeContentRenderContext}, it extends the
126
+ * shared {@link PresentationContext} (`presentation` / `isFullscreen` / `isModal`), so a group
127
+ * header can also render leaner in the phone fullscreen overlay.
128
+ */
129
+ export declare interface GroupLabelRenderContext<T = any> extends PresentationContext {
130
+ /** The group name (identical to the callback's first argument). */
131
+ groupName: string;
132
+ /** The group's currently-visible (filtered) members, in render order. */
133
+ members: T[];
134
+ /** The subset of `members` that are currently selected (includes disabled-but-selected). */
135
+ selectedMembers: T[];
136
+ /** `selectedMembers.length` — the number to show "behind the group title". */
137
+ selectedCount: number;
138
+ /** `members.length` — total visible members in the group. */
139
+ memberCount: number;
140
+ /** Visible, non-disabled members — the cascade "select-all" denominator. */
141
+ selectableCount: number;
142
+ /** Tristate roll-up of the group under cascade selection. */
143
+ checkState: 'checked' | 'indeterminate' | 'unchecked';
144
+ }
145
+
117
146
  export declare const initLogger: Logger;
118
147
 
119
148
  export declare const interactionLogger: Logger;
@@ -191,6 +220,25 @@ declare interface MultiSelectConfig<T = any> {
191
220
  getDisplayValueCallback?: (item: T) => string;
192
221
  /** Callback to customize badge display text (defaults to display value if not provided) */
193
222
  getBadgeDisplayCallback?: (item: T) => string;
223
+ /**
224
+ * Order of the CURRENTLY-SELECTED items *where they are displayed* — badges, partial mode
225
+ * (i.e. which items sit behind the "+N more" badge), and the selected-items popover. This is a
226
+ * display concern only: `getValue()`, the form output, and `getSelected()` always keep
227
+ * as-selected (insertion) order regardless of this setting, and the options dropdown is never
228
+ * reordered.
229
+ * - `as-selected` (default) — the order items were picked.
230
+ * - `label-asc` / `label-desc` — by the badge label, A→Z / Z→A (locale-aware).
231
+ * - `member` — by the `selectedOrderMember` property (or `getSelectedOrderCallback`); numeric
232
+ * keys sort numerically, everything else with a locale string compare.
233
+ * - `custom` — delegate to `selectedOrderCompareCallback`.
234
+ */
235
+ selectedOrder?: 'as-selected' | 'label-asc' | 'label-desc' | 'member' | 'custom';
236
+ /** Property name used as the sort key when `selectedOrder === 'member'` (selected-items display only). */
237
+ selectedOrderMember?: string;
238
+ /** Extract the sort key when `selectedOrder === 'member'` (overrides `selectedOrderMember`). */
239
+ getSelectedOrderCallback?: (item: T) => string | number;
240
+ /** Comparator used when `selectedOrder === 'custom'`; standard `(a,b) => number` contract. */
241
+ selectedOrderCompareCallback?: (a: T, b: T) => number;
194
242
  /**
195
243
  * Member property name for a "full title" — a fully-qualified label that ships with the
196
244
  * data (e.g. a breadcrumb like "Fruit / Pome fruit / Apple"). It is never computed by the
@@ -204,6 +252,13 @@ declare interface MultiSelectConfig<T = any> {
204
252
  getBadgeClassCallback?: (item: T) => string | string[];
205
253
  /** Callback to inject custom CSS into Shadow DOM - return CSS string for styling custom classes */
206
254
  customStylesCallback?: () => string;
255
+ /**
256
+ * Static CSS string injected into the Shadow DOM (attribute alternative to
257
+ * `customStylesCallback`, via the `custom-styles` attribute). The value is a
258
+ * raw stylesheet — selectors and all — dropped verbatim into the same
259
+ * replaceable style slot. `customStylesCallback` wins when both are set.
260
+ */
261
+ customStyles?: string;
207
262
  /** Member property name for search value extraction */
208
263
  searchValueMember?: string;
209
264
  /** Callback to extract search value from item */
@@ -250,9 +305,10 @@ declare interface MultiSelectConfig<T = any> {
250
305
  */
251
306
  getIsSelectableCallback?: (node: LTreeNode<T>) => boolean;
252
307
  /**
253
- * Tree checkbox interaction. `independent` (default) toggles only the clicked
254
- * node. `cascade` checks a node's whole subtree and shows a tristate
255
- * (checked / indeterminate / unchecked) box on branches. Tree + multiple only.
308
+ * Tree checkbox interaction. `cascade` (default) checks a node's whole subtree
309
+ * and shows a tristate (checked / indeterminate / unchecked) box on branches —
310
+ * what most tree-select UIs do. `independent` toggles only the clicked node.
311
+ * Tree + multiple only (no subtree to cascade otherwise). Unset → cascade.
256
312
  */
257
313
  checkboxMode?: 'independent' | 'cascade';
258
314
  /**
@@ -270,8 +326,23 @@ declare interface MultiSelectConfig<T = any> {
270
326
  groupMember?: string;
271
327
  /** Callback to extract group from item */
272
328
  getGroupCallback?: (item: T) => string;
273
- /** Callback to customize group label content (can return HTML) */
274
- renderGroupLabelContentCallback?: (groupName: string) => string | HTMLElement;
329
+ /**
330
+ * Callback to customize group label content (can return HTML). Receives the group name and a
331
+ * {@link GroupLabelRenderContext} with the group's members and selection (e.g. `selectedCount`),
332
+ * so a custom header can show a per-group count. The second argument is additive — existing
333
+ * one-argument callbacks keep working.
334
+ */
335
+ renderGroupLabelContentCallback?: (groupName: string, context: GroupLabelRenderContext<T>) => string | HTMLElement;
336
+ /**
337
+ * Group-header selection in a flat (non-tree) grouped, multi-select list.
338
+ * - `none` (default) — group headers are inert labels.
339
+ * - `cascade` — each header shows a **tristate** checkbox that checks/unchecks
340
+ * all of that group's currently-visible members. The group itself is never a
341
+ * selected value (`getValue()`/badges/form carry member values only); a
342
+ * partially-selected group reads indeterminate. Flat + multiple only — no
343
+ * effect in tree mode (use `checkboxMode`) or single-select.
344
+ */
345
+ groupSelectMode?: 'none' | 'cascade';
275
346
  /** Member property name for disabled state extraction */
276
347
  disabledMember?: string;
277
348
  /** Callback to extract disabled state from item */
@@ -291,12 +362,22 @@ declare interface MultiSelectConfig<T = any> {
291
362
  * selected-items popover keeps using renderSelectedItemContentCallback).
292
363
  */
293
364
  renderBadgeCallback?: (item: T, context: BadgeContentRenderContext) => string | HTMLElement | null | undefined;
294
- /** Custom renderer for selected item content in popover - return HTML string or HTMLElement */
295
- renderSelectedItemContentCallback?: (item: T) => string | HTMLElement;
365
+ /**
366
+ * Custom renderer for selected item content in the selected-items popover — return HTML string
367
+ * or HTMLElement. Receives a {@link BadgeContentRenderContext} (2nd arg) since a popover item
368
+ * is rendered through the same badge path: `isInPopover` is `true`, plus the shared
369
+ * presentation fields. The second argument is additive; one-argument callbacks keep working.
370
+ */
371
+ renderSelectedItemContentCallback?: (item: T, context: BadgeContentRenderContext) => string | HTMLElement;
296
372
  /** Callback to add custom CSS classes to selected items in popover - return string or array of class names */
297
373
  getSelectedItemClassCallback?: (item: T) => string | string[];
298
- /** Custom renderer for selected item display in single-select mode - return plain text */
299
- renderSelectedContentCallback?: (item: T) => string;
374
+ /**
375
+ * Custom renderer for the selected item display in single-select mode — return plain text (it
376
+ * becomes the input value). Receives a {@link SelectedContentRenderContext} (2nd arg) carrying
377
+ * the shared presentation fields. The second argument is additive; one-argument callbacks keep
378
+ * working.
379
+ */
380
+ renderSelectedContentCallback?: (item: T, context: SelectedContentRenderContext) => string;
300
381
  /** HTML form field ID/name for hidden input */
301
382
  formFieldId?: string;
302
383
  /**
@@ -595,8 +676,22 @@ declare interface MultiSelectConfig<T = any> {
595
676
  onDeselect?: ((option: T) => void) | null;
596
677
  /** Event handler: the selection set changed (fire-and-forget). Mirrors the bubbling `change` CustomEvent on the element. */
597
678
  onChange?: ((selectedOptions: T[]) => void) | null;
598
- /** Callback to format count badge text (for i18n/pluralization). When moreCount is provided, it's for the "+X more" badge in partial mode. */
679
+ /**
680
+ * Formats the badges-area count/summary text: the `count` mode badge ("N selected") and the
681
+ * partial-mode "+X more" badge (when `moreCount` is provided). NOT the small `[N]` chip — that's
682
+ * {@link getCountLabelCallback}. For i18n/pluralization.
683
+ */
599
684
  getCounterCallback?: ((count: number, moreCount?: number) => string) | null;
685
+ /**
686
+ * Formats the small count CHIP shared by the in-input counter (`show-counter`) and each group
687
+ * header's per-group count. Distinct from {@link getCounterCallback}, which formats the
688
+ * badges-area "N selected" / "+X more" text. Receives `selected` and `total`: for the in-input
689
+ * counter `total` is the whole option list; for a group header it's that group's member count.
690
+ * Return the label as plain text. Default `[selected]` (e.g. `[3]`). Set it to
691
+ * `` (s, t) => `${s}/${t}` `` for an "x / y" style. One callback drives both so they always
692
+ * read the same way.
693
+ */
694
+ getCountLabelCallback?: ((selected: number, total: number) => string) | null;
600
695
  /** Enable tooltips on selected item badges (internal: isBadgeTooltipsEnabled) */
601
696
  isBadgeTooltipsEnabled?: boolean;
602
697
  /** Callback to generate custom tooltip content for a badge */
@@ -928,7 +1023,7 @@ export { observeViewport }
928
1023
  * render leaner content in the phone overlay. Reactive: swapping presentation re-renders and
929
1024
  * re-invokes the callback with the new value.
930
1025
  */
931
- declare interface OptionContentRenderContext extends PresentationContext {
1026
+ export declare interface OptionContentRenderContext extends PresentationContext {
932
1027
  /** Index of the option in the filtered list */
933
1028
  index: number;
934
1029
  /** Whether the option is currently selected */
@@ -1014,6 +1109,13 @@ export declare type SearchInputMode = 'normal' | 'readonly' | 'hidden';
1014
1109
  */
1015
1110
  export declare type SearchMode = 'filter' | 'navigate';
1016
1111
 
1112
+ /**
1113
+ * Context handed to `renderSelectedContentCallback` (single-select selected-value display). Carries
1114
+ * only the shared {@link PresentationContext} fields (`presentation` / `isFullscreen` / `isModal`),
1115
+ * so the single-select label can render leaner in the phone fullscreen overlay.
1116
+ */
1117
+ export declare type SelectedContentRenderContext = PresentationContext;
1118
+
1017
1119
  /**
1018
1120
  * Set the level of one category. Accepts the full prefixed name
1019
1121
  * (`MULTISELECT:UI`) or the bare suffix (`UI`) — both normalize to the category
@@ -1136,6 +1238,14 @@ export declare class WebMultiSelect<T = any> {
1136
1238
  */
1137
1239
  private getItemFullTitle;
1138
1240
  private getItemSearchValue;
1241
+ /** Sort key for `selectedOrder === 'member'` (member/callback pattern). */
1242
+ private getItemSortKey;
1243
+ /**
1244
+ * Selected options in the order they should be DISPLAYED (badges / partial "+N more" / popover).
1245
+ * Never mutates state — always returns a fresh array. Display concern only: getValue()/form
1246
+ * output/getSelected() keep as-selected (insertion) order. See `selectedOrder`.
1247
+ */
1248
+ private getOrderedSelectedOptions;
1139
1249
  private getItemIcon;
1140
1250
  private getItemSubtitle;
1141
1251
  private getItemGroup;
@@ -1158,8 +1268,10 @@ export declare class WebMultiSelect<T = any> {
1158
1268
  private buildTree;
1159
1269
  /**
1160
1270
  * Whether cascade checkbox mode is active: a multi-select tree with
1161
- * `checkbox-mode="cascade"`. Checking a node then toggles its whole subtree
1162
- * and branches show a tristate box.
1271
+ * `checkbox-mode` NOT set to `independent`. Checking a node then toggles its
1272
+ * whole subtree and branches show a tristate box. Cascade is the DEFAULT
1273
+ * (unset → cascade); opt out per-instance with `checkbox-mode="independent"`.
1274
+ * Only ever active in tree + multiple — no subtree to cascade otherwise.
1163
1275
  */
1164
1276
  private isCascadeMode;
1165
1277
  private cascadePolicy;
@@ -1250,6 +1362,46 @@ export declare class WebMultiSelect<T = any> {
1250
1362
  * Check if any options have groups
1251
1363
  */
1252
1364
  private hasGroups;
1365
+ /**
1366
+ * Whether the flat-group cascade checkbox is active: a multi-select, grouped,
1367
+ * non-tree list with `group-select-mode="cascade"`. When on, each group header
1368
+ * gets a tristate checkbox that toggles all of that group's visible members.
1369
+ * Tree mode has its own `checkbox-mode` cascade, so this stays flat-only.
1370
+ */
1371
+ private isGroupCascadeActive;
1372
+ /**
1373
+ * Tristate check-state of a group from its members: `checked` if every
1374
+ * non-disabled member is selected, `unchecked` if none are, else
1375
+ * `indeterminate`. Disabled members are excluded from the denominator so a
1376
+ * group with a stuck-disabled member can still read fully checked. An empty
1377
+ * (or all-disabled) group reads `unchecked`.
1378
+ */
1379
+ private groupCheckState;
1380
+ /**
1381
+ * Selection roll-up for a flat group's (visible) members: which are selected, how many, and
1382
+ * the tristate check-state. `selectedCount` counts every selected member (including a
1383
+ * disabled-but-selected one) — it's the "N behind the group title". `checkState` excludes
1384
+ * disabled members from its denominator (mirrors the select-all), so a group with a stuck
1385
+ * disabled member can still read fully `checked`. Shared by the header count, the tristate
1386
+ * checkbox, and the `renderGroupLabelContentCallback` context.
1387
+ */
1388
+ private groupSelectionInfo;
1389
+ /**
1390
+ * Formats the small count chip shared by the in-input counter and the per-group header count.
1391
+ * Default `[selected]` (matches the historical in-input `[N]`); a `getCountLabelCallback` can
1392
+ * switch both to e.g. `selected/total`.
1393
+ */
1394
+ private formatCountLabel;
1395
+ /** Trailing count chip for a group header — any grouped list (rendered only when >0 selected). */
1396
+ private groupCountHtml;
1397
+ /**
1398
+ * Shared markup for a `.ms__checkbox` input — the single source of truth for option rows, tree
1399
+ * nodes, and group headers. Indeterminate is a pure CSS state (the box is `appearance: none`, so
1400
+ * no native `input.indeterminate` is needed — virtual-scroll-safe) plus `aria-checked="mixed"`.
1401
+ */
1402
+ private checkboxHtml;
1403
+ /** Group-header tristate checkbox (maps the group's roll-up state onto `checkboxHtml`). */
1404
+ private groupCheckboxHtml;
1253
1405
  private renderDropdown;
1254
1406
  /**
1255
1407
  * Round the OUTER corners of the row at the very top and the row at the very
@@ -1479,6 +1631,16 @@ export declare class WebMultiSelect<T = any> {
1479
1631
  private deselectOption;
1480
1632
  private selectAll;
1481
1633
  clearAll(): void;
1634
+ /**
1635
+ * Flat-group cascade toggle: check or uncheck every (visible) member of a group
1636
+ * in one shot. If the group is fully checked → deselect all its members; else →
1637
+ * select all its non-disabled members. Operates on the currently-filtered
1638
+ * members (same scope as Select-All) and, like Select-All / Clear-All,
1639
+ * batch-mutates then fires a single `commit` — so one render and one `change`
1640
+ * event, and it deliberately bypasses the per-item beforeSelect/beforeDeselect
1641
+ * veto. The group name itself is never added to the selection.
1642
+ */
1643
+ private toggleGroup;
1482
1644
  /**
1483
1645
  * Inline clear (✕) handler: wipe the whole selection and any search text, then
1484
1646
  * restore focus to the input. clearAll() → commit() → renderBadges() already
@@ -1552,6 +1714,22 @@ export declare class WebMultiSelect<T = any> {
1552
1714
  * so they don't actually break the sheet.
1553
1715
  */
1554
1716
  private warnFullscreenContainingBlock;
1717
+ /**
1718
+ * Re-anchor an already-open floating dropdown from scratch so a frozen placement
1719
+ * is re-evaluated against the panel's CURRENT height.
1720
+ *
1721
+ * Why it's needed: an async `searchCallback` opens the panel while it's still
1722
+ * empty / showing the loader — short, so it fits below the input and (with the
1723
+ * default `lock-placement`) freezes to `bottom`. When results arrive the panel
1724
+ * grows to full height, but the frozen placement pins it below the input, so it
1725
+ * overflows the viewport bottom instead of flipping above into the free space.
1726
+ * `renderDropdown()` only rewrites the inner HTML; it never re-anchors. Tearing
1727
+ * down and recreating the anchor re-runs core's flip-on-first-compute against the
1728
+ * new height (picking the side that fits), then re-freezes — so `lock-placement`
1729
+ * still holds for the common case (panels that open already-populated, e.g. local
1730
+ * filtering, never hit this path). No-op unless a floating dropdown is open.
1731
+ */
1732
+ private repositionDropdown;
1555
1733
  private positionDropdown;
1556
1734
  /**
1557
1735
  * Switch how the open panels are presented. 'floating' anchors them to the input