@keenmate/web-multiselect 1.9.0 → 1.11.0

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/README.md CHANGED
@@ -7,6 +7,30 @@ A lightweight, accessible multiselect web component with typeahead search, RTL l
7
7
 
8
8
  > **⚠️ Security Notice:** This component intentionally allows raw HTML in rendering callbacks to give developers full control over content display. If you display user-generated content, you must sanitize it yourself. See [HTML Injection (XSS) Notice](#html-injection-xss-notice) for the complete list of affected callbacks.
9
9
 
10
+ ## What's New in v1.11.0
11
+
12
+ - **OS-aware light/dark defaults via `light-dark()`** — set `color-scheme: dark` on your page (`:root`, `body`, etc.) and the multiselect picks readable dark text/background colors automatically. No more enumerating ~15 `--base-*` overrides just to get usable defaults on a dark theme.
13
+ - **Drift-detection warning for positioning edge cases** — if an exotic ancestor CSS property (e.g. `contain: paint`, or `container-type` in certain shadow-DOM layouts) makes the dropdown land somewhere other than where the library told the browser to put it, a `console.warn` fires once with the likely culprit element and an actionable fix suggestion.
14
+ - **Dropdown no longer stranded to the side when an ancestor uses `container-type`** — Floating UI was walking up to a `container-type: inline-size` ancestor (notably pure-admin's `.pa-layout__main`), but the browser wouldn't actually anchor the fixed panel there. The library now uses a custom `getOffsetParent` that only walks up properties browsers reliably honor for fixed positioning.
15
+ - **Dropdown no longer opens shifted to the side of its input** — when the dropdown's natural content was wider than the input, Floating UI's `shift()` middleware was measuring the unclamped panel and pushing it left, then the subsequent width clamp left it stranded next to the input. Panel sizing now happens before positioning. Same fix applies to the selected-items popover.
16
+ - **`--base-*` taxonomy aligned with KeenMate cross-component naming** (theming change — see CHANGELOG migration table): `--base-primary-bg` → `--base-hover-bg`, `--base-primary-bg-hover` → `--base-active-bg`. `--base-dropdown-bg` and `--base-tooltip-bg` continue to work; new chain fallbacks to `--base-elevated-bg` / `--base-inverse-bg`.
17
+ - **Option hover stays visible on dark themes** — `--ms-primary-bg` now mixes 8% of the text color into the main background by default, so the hover is always a visible step toward the text. No more invisible hover when the consumer forgets to override `--base-hover-bg`.
18
+ - **New `examples-positioning.html`** — walks through baseline / transformed-ancestor / container-type / drift-detection scenarios.
19
+ - **New dark-mode e2e suite** — 4 specs verifying WCAG-AA option contrast on a dark page across fully-themed, minimal-override, and pure OS-inheritance configurations.
20
+
21
+ ## What's New in v1.10.0
22
+
23
+ - **`data-options` attribute on `<web-multiselect>`** — set options declaratively from HTML, no JS bootstrap required (works alongside `initial-values` for pure-HTML / server-rendered / SharePoint workbench scenarios).
24
+ - **`form.reset()` now clears the selection** — the element is now form-associated (`static formAssociated = true` + `ElementInternals` + `formResetCallback()`).
25
+ - **Dropdown / hint / selected popover no longer clipped inside scrollable ancestors** — Floating UI now uses `strategy: 'fixed'` for all three panels, so they escape `overflow: hidden|auto|scroll` containers (e.g. SharePoint Framework workbenches).
26
+ - **`initial-values` now works when options arrive after init** — values are reconciled on every `options` mutation, not just at construction.
27
+ - **Remove / close (×) buttons render as SVG masks** — pixel-centered regardless of font; color still flows through the existing `--ms-*-color` variables via `currentColor`; three new `--ms-*-icon-size` variables for theming.
28
+ - **Keyboard navigation now keeps working after a mouse click on an option** — previously, clicking an option moved focus from the search input to the option's checkbox (knocking the `keydown` listener offline) *and* left `focusedIndex` at its pre-click value, so subsequent ArrowDown / ArrowUp / Enter went nowhere visible. Click now anchors `focusedIndex` to the clicked option and refocuses the search input, so arrow keys continue from where you clicked and Enter toggles the option under the cursor.
29
+ - **Count-clear / popover-close hover backdrop now matches the rest of the component** — was a circle (`border-radius: 50%`), now a small rounded rectangle (`--ms-border-radius-sm`) consistent with every other interactive element. Themes that prefer the circle can set `--ms-count-clear-border-radius` and `--ms-selected-popover-close-border-radius` back to `50%`.
30
+ - **Keyboard `Enter` respects disabled options** — previously only the click handler did.
31
+ - **End-to-end test suite** — 114 Playwright specs across 19 fixture pages (`npm run test:e2e`).
32
+ - **`THEMING.md`** — new reference cataloguing every theme-able component state and the CSS variables that drive it.
33
+
10
34
  ## Features
11
35
 
12
36
  - 📝 **Declarative HTML** - Use standard `<option>` and `<optgroup>` elements - no JavaScript required for simple cases!
@@ -1640,8 +1664,9 @@ KeenMate components support a **two-layer theming architecture**:
1640
1664
  :root {
1641
1665
  /* Base layer - single source of truth */
1642
1666
  --base-accent-color: #3b82f6;
1643
- --base-primary-bg: #ffffff;
1644
- --base-text-primary: #111827;
1667
+ --base-main-bg: #ffffff;
1668
+ --base-hover-bg: #f3f4f6;
1669
+ --base-text-color-1: #111827;
1645
1670
 
1646
1671
  /* Components reference base layer */
1647
1672
  --ms-accent-color: var(--base-accent-color);
@@ -1766,7 +1791,7 @@ For the complete list of all available CSS variables, see:
1766
1791
 
1767
1792
  | Variable | Default | Description |
1768
1793
  |----------|---------|-------------|
1769
- | `--ms-dropdown-bg` | `var(--base-dropdown-bg, #ffffff)` | Dropdown background |
1794
+ | `--ms-dropdown-bg` | `var(--base-dropdown-bg, var(--base-elevated-bg, light-dark(#ffffff, #1a1a1a)))` | Dropdown background (auto-adapts to OS dark mode) |
1770
1795
  | `--ms-dropdown-border` | `var(--ms-border-color)` | Dropdown border color |
1771
1796
  | `--ms-dropdown-shadow` | (box shadow) | Dropdown shadow |
1772
1797
  | `--ms-dropdown-max-height` | `20rem` | Max height of dropdown |
@@ -1791,7 +1816,9 @@ For the complete list of all available CSS variables, see:
1791
1816
  | `--ms-badge-font-size` | `0.75rem` | Badge font size |
1792
1817
  | `--ms-badge-border-radius` | `0.375rem` | Badge border radius |
1793
1818
  | `--ms-badge-remove-bg` | `var(--ms-accent-color)` | Remove button background |
1794
- | `--ms-badge-remove-color` | `var(--ms-text-color-on-accent)` | Remove button color |
1819
+ | `--ms-badge-remove-color` | `var(--ms-text-color-on-accent)` | Remove button (X) color — applied to the SVG via `currentColor` |
1820
+ | `--ms-badge-remove-icon-size` | `calc(1.0 * var(--ms-rem))` | Size of the X glyph inside the remove button |
1821
+ | `--ms-icon-remove` | (inline SVG `url(...)`) | The X mask SVG; override to swap the glyph shape (alpha-only — color comes from `--ms-badge-remove-color`) |
1795
1822
  | `--ms-badge-counter-text-bg` | `var(--ms-primary-bg)` | BadgeCounter text background ("+X more") |
1796
1823
  | `--ms-badge-counter-text-color` | `var(--ms-text-color-3)` | BadgeCounter text color |
1797
1824
  | `--ms-badge-counter-remove-bg` | `var(--ms-text-color-3)` | BadgeCounter remove button background |
@@ -1836,7 +1863,7 @@ For the complete list of all available CSS variables, see:
1836
1863
 
1837
1864
  | Variable | Default | Description |
1838
1865
  |----------|---------|-------------|
1839
- | `--ms-tooltip-bg` | `var(--base-tooltip-bg, #333333)` | Tooltip background color |
1866
+ | `--ms-tooltip-bg` | `var(--base-tooltip-bg, var(--base-inverse-bg, light-dark(#333333, #f5f5f5)))` | Tooltip background (auto-adapts to OS dark mode) |
1840
1867
  | `--ms-tooltip-color` | `var(--ms-tooltip-text-color)` | Tooltip text color |
1841
1868
  | `--ms-tooltip-padding` | `0.5rem 0.75rem` | Tooltip padding |
1842
1869
  | `--ms-tooltip-border-radius` | `0.375rem` | Tooltip border radius |
@@ -14,7 +14,8 @@
14
14
  { "name": "base-text-color-4", "required": false, "usage": "Quaternary text (placeholders)" },
15
15
  { "name": "base-text-color-on-accent", "required": false, "usage": "Text on accent backgrounds (badges, checkboxes)" },
16
16
  { "name": "base-main-bg", "required": true, "usage": "Primary background, hint background, actions background" },
17
- { "name": "base-hover-bg", "required": false, "usage": "Hover states for options, badges, buttons" },
17
+ { "name": "base-hover-bg", "required": false, "usage": "Hover background (drives --ms-primary-bg: option hover/focus, action-button hover, counter hover)" },
18
+ { "name": "base-active-bg", "required": false, "usage": "Active/pressed background (drives --ms-primary-bg-hover: selected-active, counter badge)" },
18
19
  { "name": "base-disabled-bg", "required": false, "usage": "Disabled/readonly surface backgrounds" },
19
20
  { "name": "base-border-color", "required": true, "usage": "Input, dropdown, action button borders" },
20
21
  { "name": "base-border", "required": false, "usage": "Full border shorthand" },
@@ -25,10 +26,12 @@
25
26
  { "name": "base-input-border-focus", "required": false, "usage": "Input border when focused" },
26
27
  { "name": "base-input-placeholder-color", "required": false, "usage": "Placeholder text color" },
27
28
  { "name": "base-input-bg-disabled", "required": false, "usage": "Disabled input background" },
28
- { "name": "base-dropdown-bg", "required": false, "usage": "Dropdown and popover backgrounds" },
29
+ { "name": "base-dropdown-bg", "required": false, "usage": "Dropdown and popover backgrounds (primary; falls back to base-elevated-bg)" },
30
+ { "name": "base-elevated-bg", "required": false, "usage": "Elevated surface (fallback for dropdown and popover backgrounds)" },
29
31
  { "name": "base-dropdown-border", "required": false, "usage": "Dropdown border" },
30
32
  { "name": "base-dropdown-box-shadow", "required": false, "usage": "Dropdown shadow" },
31
- { "name": "base-tooltip-bg", "required": false, "usage": "Tooltip background" },
33
+ { "name": "base-tooltip-bg", "required": false, "usage": "Tooltip background (primary; falls back to base-inverse-bg)" },
34
+ { "name": "base-inverse-bg", "required": false, "usage": "Inverse surface (fallback for tooltip background)" },
32
35
  { "name": "base-tooltip-text-color", "required": false, "usage": "Tooltip text color" },
33
36
  { "name": "base-font-family", "required": false, "usage": "All text in component" },
34
37
  { "name": "base-font-size-2xs", "required": false, "usage": "Smallest text (multiplier)" },
@@ -69,8 +72,8 @@
69
72
  { "name": "ms-text-primary", "category": "text", "usage": "Legacy alias for text-color-1" },
70
73
  { "name": "ms-text-secondary", "category": "text", "usage": "Legacy alias for text-color-3" },
71
74
 
72
- { "name": "ms-primary-bg", "category": "surface", "usage": "Primary background color" },
73
- { "name": "ms-primary-bg-hover", "category": "surface", "usage": "Primary background hover" },
75
+ { "name": "ms-primary-bg", "category": "surface", "usage": "Hover/focus background (options, action buttons, counter); reads --base-hover-bg" },
76
+ { "name": "ms-primary-bg-hover", "category": "surface", "usage": "Active/stronger background (counter badge text); reads --base-active-bg" },
74
77
 
75
78
  { "name": "ms-border-color", "category": "border", "usage": "Default border color" },
76
79
  { "name": "ms-border", "category": "border", "usage": "Full border shorthand" },
@@ -270,10 +273,11 @@
270
273
  { "name": "ms-badge-remove-bg", "category": "badge", "usage": "Badge remove button background" },
271
274
  { "name": "ms-badge-remove-color", "category": "badge", "usage": "Badge remove button color" },
272
275
  { "name": "ms-badge-remove-border", "category": "badge", "usage": "Badge remove button border" },
273
- { "name": "ms-badge-remove-font-size", "category": "badge", "usage": "Badge remove button font size" },
276
+ { "name": "ms-badge-remove-font-size", "category": "badge", "usage": "Badge remove button font size (unused since 1.10.0 — kept for backward-compat)" },
277
+ { "name": "ms-badge-remove-icon-size", "category": "badge", "usage": "Badge remove X icon size (mask SVG)" },
274
278
  { "name": "ms-badge-remove-bg-hover", "category": "badge", "usage": "Badge remove button hover background" },
275
279
  { "name": "ms-badge-remove-box-shadow-focus", "category": "badge", "usage": "Badge remove button focus shadow" },
276
- { "name": "ms-icon-remove", "category": "badge", "usage": "Remove icon character" },
280
+ { "name": "ms-icon-remove", "category": "badge", "usage": "Remove icon as CSS url() to a mask-friendly SVG; color comes from currentColor" },
277
281
  { "name": "ms-badge-counter-bg", "category": "badge", "usage": "Counter badge background" },
278
282
  { "name": "ms-badge-counter-border", "category": "badge", "usage": "Counter badge border" },
279
283
  { "name": "ms-badge-counter-border-color", "category": "badge", "usage": "Counter badge border color" },
@@ -306,11 +310,12 @@
306
310
  { "name": "ms-count-clear-size", "category": "count", "usage": "Count clear button size" },
307
311
  { "name": "ms-count-clear-bg", "category": "count", "usage": "Count clear button background" },
308
312
  { "name": "ms-count-clear-color", "category": "count", "usage": "Count clear button color" },
309
- { "name": "ms-count-clear-font-size", "category": "count", "usage": "Count clear button font size" },
313
+ { "name": "ms-count-clear-font-size", "category": "count", "usage": "Count clear button font size (unused since 1.10.0 — kept for backward-compat)" },
314
+ { "name": "ms-count-clear-icon-size", "category": "count", "usage": "Count clear X icon size (mask SVG)" },
310
315
  { "name": "ms-count-clear-border-radius", "category": "count", "usage": "Count clear button border radius" },
311
316
  { "name": "ms-count-clear-bg-hover", "category": "count", "usage": "Count clear button hover background" },
312
317
  { "name": "ms-count-clear-color-hover", "category": "count", "usage": "Count clear button hover color" },
313
- { "name": "ms-icon-clear", "category": "count", "usage": "Clear icon character" },
318
+ { "name": "ms-icon-clear", "category": "count", "usage": "Clear icon as CSS url() to a mask-friendly SVG; defaults to var(--ms-icon-remove)" },
314
319
 
315
320
  { "name": "ms-tooltip-bg", "category": "tooltip", "usage": "Tooltip background" },
316
321
  { "name": "ms-tooltip-text-color", "category": "tooltip", "usage": "Tooltip text color" },
@@ -340,7 +345,8 @@
340
345
  { "name": "ms-popover-close-size", "category": "popover", "usage": "Popover close button size" },
341
346
  { "name": "ms-selected-popover-close-bg", "category": "popover", "usage": "Popover close button background" },
342
347
  { "name": "ms-selected-popover-close-color", "category": "popover", "usage": "Popover close button color" },
343
- { "name": "ms-selected-popover-close-font-size", "category": "popover", "usage": "Popover close button font size" },
348
+ { "name": "ms-selected-popover-close-font-size", "category": "popover", "usage": "Popover close button font size (unused since 1.10.0 — kept for backward-compat)" },
349
+ { "name": "ms-selected-popover-close-icon-size", "category": "popover", "usage": "Popover close X icon size (mask SVG)" },
344
350
  { "name": "ms-selected-popover-close-border-radius", "category": "popover", "usage": "Popover close button border radius" },
345
351
  { "name": "ms-selected-popover-close-bg-hover", "category": "popover", "usage": "Popover close button hover background" },
346
352
  { "name": "ms-selected-popover-close-color-hover", "category": "popover", "usage": "Popover close button hover color" },
package/dist/index.d.ts CHANGED
@@ -194,7 +194,7 @@ declare interface MultiSelectConfig<T = any> {
194
194
  isVirtualScrollEnabled?: boolean;
195
195
  /** Vertical alignment of checkboxes relative to option content */
196
196
  checkboxAlign?: 'top' | 'center' | 'bottom';
197
- /** Hint text shown above the input when focused */
197
+ /** Hint text shown above the input while the dropdown is open. */
198
198
  searchHint?: string;
199
199
  /** Placeholder text for the search input */
200
200
  searchPlaceholder?: string;
@@ -269,9 +269,11 @@ declare interface MultiSelectConfig<T = any> {
269
269
  }
270
270
 
271
271
  export declare class MultiSelectElement<T = any> extends BaseElement {
272
+ static formAssociated: boolean;
272
273
  private picker?;
273
274
  private containerElement?;
274
275
  private shadow;
276
+ private internals?;
275
277
  private _options?;
276
278
  private _valueMember?;
277
279
  private _getValueCallback?;
@@ -309,6 +311,14 @@ export declare class MultiSelectElement<T = any> extends BaseElement {
309
311
  private _changeCallback?;
310
312
  static get observedAttributes(): string[];
311
313
  constructor();
314
+ /**
315
+ * Called by the browser when the surrounding <form> is reset. Clears the
316
+ * picker's selection so the multiselect actually participates in the
317
+ * standard reset lifecycle. (Before form-association, reset was a no-op
318
+ * because the hidden inputs were re-stamped from internal state on every
319
+ * render.)
320
+ */
321
+ formResetCallback(): void;
312
322
  connectedCallback(): void;
313
323
  disconnectedCallback(): void;
314
324
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
@@ -543,6 +553,7 @@ export declare class WebMultiSelect<T = any> {
543
553
  private isRTL;
544
554
  private effectiveBadgesPosition;
545
555
  private justClosedViaClick;
556
+ private positioningDriftWarned;
546
557
  private dropdownCleanup;
547
558
  private hintCleanup;
548
559
  private selectedPopoverCleanup;
@@ -638,7 +649,7 @@ export declare class WebMultiSelect<T = any> {
638
649
  private selectOption;
639
650
  private deselectOption;
640
651
  private selectAll;
641
- private clearAll;
652
+ clearAll(): void;
642
653
  /**
643
654
  * Re-render and fire callbacks after a selection state change.
644
655
  * `added` / `removed` drive per-item select/deselect callbacks.
@@ -655,9 +666,30 @@ export declare class WebMultiSelect<T = any> {
655
666
  * compute then lock the resulting placement, optionally clamp by dropdownMin/MaxWidth.
656
667
  */
657
668
  private anchorFloatingPanel;
669
+ /**
670
+ * Sanity-check that the browser placed the panel where we told it to. With `position: fixed`
671
+ * and no transformed/perspective/filter ancestor, `left: ${x}px` must render at viewport-x = x.
672
+ * If the rendered position drifts, the consumer has an ancestor that establishes a fixed
673
+ * containing block but isn't on our reliable-anchors list (likely `contain: paint|layout|strict`
674
+ * or `container-type` — which the spec says creates a CB but the browser's actual behavior
675
+ * varies across shadow-DOM scenarios). We can't fix it from inside the library, but we can
676
+ * surface a clear warning so the developer knows where to look.
677
+ *
678
+ * Fires at most once per multiselect instance to avoid flooding the console during autoUpdate.
679
+ */
680
+ private verifyPanelLanded;
658
681
  private positionDropdown;
659
682
  private positionHint;
660
683
  private parseInitialSelection;
684
+ /**
685
+ * Resolve any `selectedValues` entries that don't yet have a matching
686
+ * `selectedOptions` object by looking them up in the current `allOptions`.
687
+ * Idempotent; safe to call after init *and* after `options` is replaced
688
+ * (e.g., async fetch, `searchCallback` result, or late `element.options =`
689
+ * assignment). Without this, `initial-values` declared before options
690
+ * arrive ends up with phantom values that `getValue()` can never report.
691
+ */
692
+ private reconcileSelectedOptions;
661
693
  private toggleSelectedPopover;
662
694
  private showPopover;
663
695
  private hideSelectedPopover;