@keenmate/web-multiselect 1.12.0-rc01 → 1.12.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/docs/usage.md ADDED
@@ -0,0 +1,307 @@
1
+ # Usage & API reference
2
+
3
+ This document covers the public surface of `<web-multiselect>` — declarative HTML usage, the full attribute / property / method / event tables, and the data shape.
4
+
5
+ ## Declarative (no JavaScript)
6
+
7
+ Perfect for simple forms — just use standard HTML `<option>` and `<optgroup>` elements:
8
+
9
+ ```html
10
+ <!-- Simple choice -->
11
+ <web-multiselect multiple="false">
12
+ <option value="yes">Yes</option>
13
+ <option value="no">No</option>
14
+ <option value="maybe" selected>Maybe</option>
15
+ </web-multiselect>
16
+
17
+ <!-- With icons -->
18
+ <web-multiselect>
19
+ <option value="apple" data-icon="🍎">Apple</option>
20
+ <option value="banana" data-icon="🍌" selected>Banana</option>
21
+ <option value="orange" data-icon="🍊">Orange</option>
22
+ </web-multiselect>
23
+
24
+ <!-- With groups -->
25
+ <web-multiselect>
26
+ <optgroup label="Frontend">
27
+ <option value="js" data-icon="🟨">JavaScript</option>
28
+ <option value="ts" data-icon="🔷">TypeScript</option>
29
+ </optgroup>
30
+ <optgroup label="Backend">
31
+ <option value="python" data-icon="🐍" selected>Python</option>
32
+ <option value="java" data-icon="☕">Java</option>
33
+ </optgroup>
34
+ </web-multiselect>
35
+ ```
36
+
37
+ ## Programmatic (with JavaScript)
38
+
39
+ For dynamic data and advanced features:
40
+
41
+ ```html
42
+ <!-- Multi-select -->
43
+ <web-multiselect
44
+ id="my-select"
45
+ search-placeholder="Search options..."
46
+ initial-values='["js","ts"]'>
47
+ </web-multiselect>
48
+ ```
49
+
50
+ ```typescript
51
+ // Import the component (includes styles)
52
+ import '@keenmate/web-multiselect';
53
+
54
+ // Or import styles separately if needed
55
+ import '@keenmate/web-multiselect/style.css';
56
+
57
+ const multiselect = document.querySelector('web-multiselect');
58
+
59
+ // Set options programmatically
60
+ multiselect.options = [
61
+ { value: 'js', label: 'JavaScript', icon: '🟨' },
62
+ { value: 'ts', label: 'TypeScript', icon: '🔷' },
63
+ { value: 'py', label: 'Python', icon: '🐍' }
64
+ ];
65
+
66
+ // Listen for events
67
+ multiselect.addEventListener('change', (e) => {
68
+ console.log('Selected:', e.detail.selectedOptions);
69
+ console.log('Values:', e.detail.selectedValues);
70
+ });
71
+
72
+ // Public API
73
+ const selected = multiselect.getSelected();
74
+ multiselect.setSelected(['js', 'ts']);
75
+ ```
76
+
77
+ ## Attributes
78
+
79
+ | Attribute | Type | Default | Description |
80
+ |-----------|------|---------|-------------|
81
+ | `multiple` | `boolean` | `true` | Allow multiple selections |
82
+ | `search-placeholder` | `string` | `'Search...'` | Placeholder text for search input |
83
+ | `search-hint` | `string` | - | Hint text shown above input when focused |
84
+ | `allow-groups` | `boolean` | `true` | Enable option grouping |
85
+ | `show-checkboxes` | `boolean` | `true` | Show checkboxes next to options |
86
+ | `close-on-select` | `boolean` | `false` | Close dropdown after selecting |
87
+ | `dropdown-min-width` | `string` | - | Min width for dropdown (e.g., `'20rem'`) |
88
+ | `badges-display-mode` | `'pills' \| 'count' \| 'compact' \| 'partial' \| 'none'` | `'pills'` | How to display selected items. `compact`: first item + count. `none`: no display |
89
+ | `badges-threshold` | `number` | - | Auto-switch mode when exceeded (see `badges-threshold-mode`) |
90
+ | `badges-threshold-mode` | `'count' \| 'partial'` | `'count'` | Mode after threshold: `count` shows badge, `partial` shows limited badges + more badge |
91
+ | `badges-max-visible` | `number` | `3` | Max badges shown in partial mode |
92
+ | `badges-position` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'bottom'` | Position of badges container |
93
+ | `show-counter` | `boolean` | `false` | Show `[3]` badge next to toggle icon |
94
+ | `enable-badge-tooltips` | `boolean` | `false` | Enable tooltips on selected badges |
95
+ | `badge-tooltip-placement` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'top'` | Tooltip placement relative to badge |
96
+ | `badge-tooltip-delay` | `number` | `100` | Delay in ms before showing tooltip |
97
+ | `badge-tooltip-offset` | `number` | `8` | Distance in pixels between badge and tooltip |
98
+ | `max-height` | `string` | `'20rem'` | Maximum height of dropdown |
99
+ | `empty-message` | `string` | `'No results found'` | Message when no options found |
100
+ | `loading-message` | `string` | `'Loading...'` | Message while loading async data |
101
+ | `min-search-length` | `number` | `0` | Minimum search length for async |
102
+ | `keep-options-on-search` | `boolean` | `true` | Keep initial options visible when `searchCallback` is active (hybrid search) |
103
+ | `should-keep-search-on-close` | `boolean` | `true` | Preserve search text and filtered results when dropdown closes |
104
+ | `sticky-actions` | `boolean` | `true` | Keep action buttons fixed at top while scrolling |
105
+ | `actions-layout` | `'nowrap' \| 'wrap'` | `'nowrap'` | Layout mode for action buttons: `nowrap` (single row) or `wrap` (multi-row) |
106
+ | `lock-placement` | `boolean` | `true` | Lock dropdown placement after first open to prevent flipping |
107
+ | `enable-search` | `boolean` | `true` | Enable/disable search functionality |
108
+ | `search-input-mode` | `'normal' \| 'readonly' \| 'hidden'` | `'normal'` | Search input display mode |
109
+ | `search-mode` | `'filter' \| 'navigate'` | `'filter'` | Search behavior: `filter` hides non-matches, `navigate` jumps to matches |
110
+ | `allow-add-new` | `boolean` | `false` | Allow adding new options not in the list |
111
+ | `value-member` | `string` | - | Property name for value/ID extraction from custom objects |
112
+ | `display-value-member` | `string` | - | Property name for display text extraction from custom objects |
113
+ | `search-value-member` | `string` | - | Property name for search text extraction from custom objects |
114
+ | `icon-member` | `string` | - | Property name for icon extraction from custom objects |
115
+ | `subtitle-member` | `string` | - | Property name for subtitle extraction from custom objects |
116
+ | `group-member` | `string` | - | Property name for group extraction from custom objects |
117
+ | `disabled-member` | `string` | - | Property name for disabled state extraction from custom objects |
118
+ | `name` | `string` | - | HTML form field name for form integration (creates hidden input) |
119
+ | `value-format` | `'json' \| 'csv' \| 'array'` | `'json'` | Format for form value serialization |
120
+ | `initial-values` | `string` (JSON array) | - | Pre-selected values |
121
+ | `enable-virtual-scroll` | `boolean` | `false` | Enable virtual scrolling for large datasets |
122
+ | `virtual-scroll-threshold` | `number` | `100` | Minimum items before virtual scroll activates |
123
+ | `option-height` | `number` | `50` | Fixed height for each option in pixels (required for virtual scroll) |
124
+ | `virtual-scroll-buffer` | `number` | `10` | Buffer size — extra items rendered above/below viewport |
125
+
126
+ ## Properties
127
+
128
+ ```typescript
129
+ // Get/set options
130
+ multiselect.options = [
131
+ { value: 'js', label: 'JavaScript' },
132
+ { value: 'ts', label: 'TypeScript' }
133
+ ];
134
+
135
+ // Async data loading
136
+ multiselect.onSearch = async (searchTerm) => {
137
+ const response = await fetch(`/api/search?q=${searchTerm}`);
138
+ return await response.json();
139
+ };
140
+
141
+ // Pre-process search terms before calling searchCallback
142
+ multiselect.beforeSearchCallback = (searchTerm) => {
143
+ // Remove accents: "café" → "cafe"
144
+ const normalized = searchTerm.normalize('NFD').replace(/[̀-ͯ]/g, '');
145
+
146
+ // Block search if too short (return null to prevent search)
147
+ if (normalized.length < 2) return null;
148
+
149
+ return normalized; // Return transformed term
150
+ };
151
+
152
+ // Event callbacks
153
+ multiselect.onSelect = (option) => {
154
+ console.log('Selected:', option);
155
+ };
156
+
157
+ multiselect.onDeselect = (option) => {
158
+ console.log('Deselected:', option);
159
+ };
160
+
161
+ multiselect.onChange = (selectedOptions) => {
162
+ console.log('Changed:', selectedOptions);
163
+ };
164
+
165
+ // Badge display customization (show different text in badges vs dropdown)
166
+ multiselect.getBadgeDisplayCallback = (item) => {
167
+ // Show shorter text in badges (e.g., just name instead of "name (email)")
168
+ return item.name; // Dropdown might show "John Doe (john@example.com)"
169
+ };
170
+
171
+ // Badge tooltip customization
172
+ multiselect.getBadgeTooltipCallback = (item) => {
173
+ return `${item.label} - ${item.subtitle}`;
174
+ };
175
+
176
+ // Action buttons (Select All, Clear All, custom actions)
177
+ multiselect.actionButtons = [
178
+ {
179
+ action: 'select-all',
180
+ text: 'Select All',
181
+ tooltip: 'Select all items',
182
+ cssClass: 'my-custom-class',
183
+ isVisibleCallback: (multiselect) => multiselect.getSelected().length < 5 // Hide if 5+ selected
184
+ },
185
+ {
186
+ action: 'clear-all',
187
+ text: 'Clear All',
188
+ tooltip: 'Clear selection',
189
+ isVisible: true, // Static visibility
190
+ isDisabled: false // Static disabled state
191
+ },
192
+ {
193
+ action: 'custom',
194
+ text: 'Invert',
195
+ tooltip: 'Invert selection',
196
+ onClick: (multiselect) => {
197
+ // Custom action - invert selection
198
+ const allValues = multiselect.options.map(opt => opt.value);
199
+ const selectedValues = multiselect.getValue();
200
+ const inverted = allValues.filter(v => !selectedValues.includes(v));
201
+ multiselect.setSelected(inverted);
202
+ },
203
+ // Dynamic callbacks (take priority over static properties)
204
+ isDisabledCallback: (multiselect) => multiselect.getSelected().length === 0,
205
+ getTextCallback: (multiselect) => multiselect.getSelected().length > 0 ? 'Invert' : 'Select Items First',
206
+ getClassCallback: (multiselect) => multiselect.getSelected().length > 0 ? 'active' : 'inactive'
207
+ }
208
+ ];
209
+
210
+ // Counter i18n/pluralization
211
+ multiselect.getCounterCallback = (count, moreCount) => {
212
+ if (moreCount !== undefined) {
213
+ return `+${moreCount} more`; // Partial mode badge
214
+ }
215
+ return `${count} selected`; // Count mode display
216
+ };
217
+
218
+ // Data extraction - Member properties (for simple property names)
219
+ multiselect.valueMember = 'id';
220
+ multiselect.displayValueMember = 'name';
221
+ multiselect.iconMember = 'icon';
222
+ multiselect.subtitleMember = 'description';
223
+ multiselect.groupMember = 'category';
224
+ multiselect.disabledMember = 'isDisabled';
225
+
226
+ // Data extraction - Callback functions (for complex logic)
227
+ multiselect.getValueCallback = (item) => item.id || item.value;
228
+ multiselect.getDisplayValueCallback = (item) => item.label || item.name;
229
+ multiselect.getSearchValueCallback = (item) => `${item.name} ${item.tags.join(' ')}`;
230
+ multiselect.getIconCallback = (item) => item.icon || '📄';
231
+ multiselect.getSubtitleCallback = (item) => `${item.price} - ${item.stock} in stock`;
232
+ multiselect.getGroupCallback = (item) => item.category;
233
+ multiselect.getDisabledCallback = (item) => item.stock === 0;
234
+
235
+ // Custom rendering - Full HTML control
236
+ multiselect.renderGroupLabelContentCallback = (groupName) => {
237
+ return `<strong>📦 ${groupName.toUpperCase()}</strong>`;
238
+ };
239
+
240
+ multiselect.renderOptionContentCallback = (item, context) => {
241
+ return `<strong>${item.name}</strong> <span class="badge">${item.status}</span>`;
242
+ };
243
+
244
+ multiselect.renderBadgeContentCallback = (item, context) => {
245
+ return context.isInPopover
246
+ ? `${item.icon} ${item.name} - ${item.description}`
247
+ : `${item.icon} ${item.name}`;
248
+ };
249
+
250
+ multiselect.renderSelectedContentCallback = (item) => {
251
+ // Customize selected item text in single-select mode (plain text only)
252
+ return item.firstName;
253
+ };
254
+
255
+ // Form integration
256
+ multiselect.name = 'selected_items';
257
+ multiselect.valueFormat = 'json'; // 'json' | 'csv' | 'array'
258
+ multiselect.getValueFormatCallback = (values) => values.join('|'); // Custom format
259
+
260
+ // Read-only properties
261
+ const selectedValue = multiselect.selectedValue; // string | number | array | null
262
+ const selectedItem = multiselect.selectedItem; // First selected item object
263
+
264
+ // Add new option callback
265
+ multiselect.addNewCallback = async (value) => {
266
+ const newOption = await fetch('/api/options', {
267
+ method: 'POST',
268
+ body: JSON.stringify({ name: value })
269
+ }).then(r => r.json());
270
+ return newOption;
271
+ };
272
+ ```
273
+
274
+ ## Methods
275
+
276
+ | Method | Description |
277
+ |--------|-------------|
278
+ | `getSelected()` | Get currently selected options as array of option objects |
279
+ | `setSelected(values: (string \| number)[])` | Set selected values by ID/value |
280
+ | `getValue()` | Get selected value(s) — returns single value in single-select mode, array in multi-select mode |
281
+ | `destroy()` | Clean up and destroy instance |
282
+
283
+ ## Events
284
+
285
+ | Event | Detail | Description |
286
+ |-------|--------|-------------|
287
+ | `select` | `{ option, selectedOptions }` | Fired when an option is selected |
288
+ | `deselect` | `{ option, selectedOptions }` | Fired when an option is deselected |
289
+ | `change` | `{ selectedOptions, selectedValues }` | Fired when selection changes |
290
+
291
+ ## Option structure
292
+
293
+ ```typescript
294
+ interface MultiSelectOption {
295
+ value: string; // Required: Unique identifier
296
+ label: string; // Required: Display text
297
+ icon?: string; // Optional: Icon or emoji
298
+ subtitle?: string; // Optional: Subtitle/description
299
+ group?: string; // Optional: Group name
300
+ disabled?: boolean; // Optional: Disable selection
301
+ }
302
+ ```
303
+
304
+ The component also accepts:
305
+
306
+ - **Tuple arrays** like `[['js', 'JavaScript'], ['ts', 'TypeScript']]` — first element becomes value, second becomes display text.
307
+ - **Arbitrary custom objects** via member attributes (`value-member`, `display-value-member`, etc.) or callbacks (`getValueCallback`, `getDisplayValueCallback`, etc.). See [examples.md → Flexible Data Handling](./examples.md#flexible-data-handling).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keenmate/web-multiselect",
3
- "version": "1.12.0-rc01",
3
+ "version": "1.12.0-rc03",
4
4
  "description": "Lightweight multiselect web component with typeahead search, rich content support, and excellent keyboard navigation",
5
5
  "type": "module",
6
6
  "main": "./dist/multiselect.umd.js",
@@ -30,6 +30,10 @@
30
30
  "files": [
31
31
  "dist",
32
32
  "src/css",
33
+ "docs/usage.md",
34
+ "docs/theming.md",
35
+ "docs/examples.md",
36
+ "docs/accessibility.md",
33
37
  "component-variables.manifest.json"
34
38
  ],
35
39
  "scripts": {
package/src/css/base.css CHANGED
@@ -10,7 +10,7 @@
10
10
  Prevent Flash of Unstyled Content by hiding undefined custom elements
11
11
  Keep element visible to prevent layout shift, but hide any text content
12
12
  */
13
- multi-select:not(:defined) {
13
+ web-multiselect:not(:defined) {
14
14
  display: block;
15
15
  min-height: calc(3.5 * var(--ms-rem)); /* 35px (md size) */
16
16
  color: transparent !important;
@@ -14,6 +14,11 @@
14
14
  ============================================================================== */
15
15
 
16
16
  :host {
17
+ /* Block layout — the wrapper-host pattern paints chrome via .ms__input,
18
+ but :host still needs an explicit display so consumers placing the
19
+ element in inline contexts see the expected layout. */
20
+ display: block;
21
+
17
22
  /* ==========================================================================
18
23
  COLOR SCHEME
19
24
  ==========================================================================
@@ -50,12 +55,6 @@
50
55
  ========================================================================== */
51
56
  font-family: var(--ms-font-family, var(--base-font-family, inherit));
52
57
 
53
- /* ==========================================================================
54
- HOST ELEMENT SIZING
55
- ==========================================================================
56
- Allows external width/height styling on <web-multiselect> element */
57
- display: block;
58
-
59
58
  /* ==========================================================================
60
59
  COLORS & THEME
61
60
  ==========================================================================