@nanahoshi/mona-ui 0.23.0 → 0.23.1

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.
Files changed (21) hide show
  1. package/fesm2022/nanahoshi-mona-ui-dropdown-list.mjs +1 -1
  2. package/fesm2022/{nanahoshi-mona-ui-grid-grid-footer-row.component--nF_Q7Bd.mjs → nanahoshi-mona-ui-grid-grid-footer-row.component-CIMXz60d.mjs} +2 -2
  3. package/fesm2022/{nanahoshi-mona-ui-grid-grid-list.component-S2qxwP2M.mjs → nanahoshi-mona-ui-grid-grid-list.component-CpeheGXU.mjs} +4 -4
  4. package/fesm2022/{nanahoshi-mona-ui-grid-grid-row.component-Dwy8hCm7.mjs → nanahoshi-mona-ui-grid-grid-row.component-B_5ay_Nz.mjs} +2 -2
  5. package/fesm2022/{nanahoshi-mona-ui-grid-grid-virtual-list.component-DArxdAah.mjs → nanahoshi-mona-ui-grid-grid-virtual-list.component-BzKywU5b.mjs} +4 -4
  6. package/fesm2022/{nanahoshi-mona-ui-grid-nanahoshi-mona-ui-grid-bduDEm-x.mjs → nanahoshi-mona-ui-grid-nanahoshi-mona-ui-grid-IewqeT4m.mjs} +8 -8
  7. package/fesm2022/nanahoshi-mona-ui-grid.mjs +1 -1
  8. package/fesm2022/nanahoshi-mona-ui-internal-filter-input.mjs +47 -7
  9. package/fesm2022/nanahoshi-mona-ui-internal-list.mjs +386 -180
  10. package/fesm2022/nanahoshi-mona-ui-internal-tree.mjs +266 -99
  11. package/fesm2022/nanahoshi-mona-ui-internal.mjs +35 -32
  12. package/fesm2022/nanahoshi-mona-ui-locales.mjs +13 -0
  13. package/fesm2022/nanahoshi-mona-ui-popup-menu.mjs +1 -1
  14. package/fesm2022/nanahoshi-mona-ui-tree-view.mjs +1 -1
  15. package/package.json +1 -1
  16. package/types/nanahoshi-mona-ui-i18n.d.ts +1 -0
  17. package/types/nanahoshi-mona-ui-internal-filter-input.d.ts +35 -1
  18. package/types/nanahoshi-mona-ui-internal-list.d.ts +82 -16
  19. package/types/nanahoshi-mona-ui-internal-tree.d.ts +50 -3
  20. package/types/nanahoshi-mona-ui-internal.d.ts +18 -4
  21. package/types/nanahoshi-mona-ui-locales.d.ts +13 -0
@@ -1,22 +1,21 @@
1
1
  import { CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll, CdkVirtualForOf } from '@angular/cdk/scrolling';
2
2
  import { NgTemplateOutlet } from '@angular/common';
3
3
  import * as i0 from '@angular/core';
4
- import { Directive, signal, computed, Injectable, inject, input, ElementRef, output, ChangeDetectionStrategy, Component, DestroyRef, viewChild, contentChild, TemplateRef, effect, untracked, afterNextRender } from '@angular/core';
4
+ import { Directive, computed, signal, linkedSignal, Injectable, inject, input, ElementRef, Component, DestroyRef, viewChild, contentChild, TemplateRef, output, effect, untracked, afterNextRender } from '@angular/core';
5
5
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
6
  import { MonaI18nService } from '@nanahoshi/mona-ui/i18n';
7
- import { isTypeaheadKey, setupTypeahead } from '@nanahoshi/mona-ui/internal';
7
+ import { createElementControlId, isTypeaheadKey, setupTypeahead } from '@nanahoshi/mona-ui/internal';
8
8
  import { FilterInputComponent } from '@nanahoshi/mona-ui/internal/filter-input';
9
9
  import { PlaceholderComponent } from '@nanahoshi/mona-ui/placeholder';
10
10
  import { Subject, ReplaySubject, fromEvent, asyncScheduler, filter, tap } from 'rxjs';
11
11
  import { twMerge } from 'tailwind-merge';
12
- import { ImmutableList, from, ImmutableSet, toImmutableSet } from '@mirei/ts-collections';
12
+ import { ImmutableList, toImmutableSet, ImmutableSet, from } from '@mirei/ts-collections';
13
13
  import { v4 } from 'uuid';
14
14
  import { cva } from 'class-variance-authority';
15
- import * as i1 from '@angular/forms';
16
- import { FormsModule } from '@angular/forms';
17
15
  import { CheckboxDirective } from '@nanahoshi/mona-ui/check-box';
18
16
 
19
17
  const LIST_DEFAULT_MESSAGES = {
18
+ filterList: "Filter list",
20
19
  noData: "No data"
21
20
  };
22
21
 
@@ -27,8 +26,7 @@ class ListFooterTemplateDirective {
27
26
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ListFooterTemplateDirective, decorators: [{
28
27
  type: Directive,
29
28
  args: [{
30
- selector: "ng-template[monaListFooterTemplate]",
31
- standalone: true
29
+ selector: "ng-template[monaListFooterTemplate]"
32
30
  }]
33
31
  }] });
34
32
 
@@ -39,8 +37,7 @@ class ListGroupHeaderTemplateDirective {
39
37
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ListGroupHeaderTemplateDirective, decorators: [{
40
38
  type: Directive,
41
39
  args: [{
42
- selector: "ng-template[monaListGroupHeaderTemplate]",
43
- standalone: true
40
+ selector: "ng-template[monaListGroupHeaderTemplate]"
44
41
  }]
45
42
  }] });
46
43
 
@@ -51,8 +48,7 @@ class ListHeaderTemplateDirective {
51
48
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ListHeaderTemplateDirective, decorators: [{
52
49
  type: Directive,
53
50
  args: [{
54
- selector: "ng-template[monaListHeaderTemplate]",
55
- standalone: true
51
+ selector: "ng-template[monaListHeaderTemplate]"
56
52
  }]
57
53
  }] });
58
54
 
@@ -63,17 +59,17 @@ class ListItemTemplateDirective {
63
59
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ListItemTemplateDirective, decorators: [{
64
60
  type: Directive,
65
61
  args: [{
66
- selector: "ng-template[monaListItemTemplate]",
67
- standalone: true
62
+ selector: "ng-template[monaListItemTemplate]"
68
63
  }]
69
64
  }] });
70
65
 
71
66
  class ListItem {
72
67
  constructor(options) {
73
68
  this.header = "";
74
- this.uid = v4();
75
69
  this.data = options.data;
70
+ this.uid = options.uid ?? v4();
76
71
  this.header = options.header ?? this.header;
72
+ this.isHeader = options.isHeader ?? this.header !== "";
77
73
  }
78
74
  }
79
75
 
@@ -90,15 +86,110 @@ predicate) => {
90
86
 
91
87
  class ListService {
92
88
  constructor() {
89
+ this.#arrangedItems = computed(() => {
90
+ const listItems = this.#listItems();
91
+ const filterText = this.filterText();
92
+ const groupableOptions = this.groupableOptions();
93
+ let enumerable = listItems;
94
+ if (filterText) {
95
+ enumerable = enumerable.where(item => this.filterItem(item, filterText));
96
+ }
97
+ if (!groupableOptions.enabled) {
98
+ return enumerable.toImmutableSet();
99
+ }
100
+ if (groupableOptions.orderBy) {
101
+ if (groupableOptions.orderByDirection === "desc") {
102
+ enumerable = enumerable.orderByDescending(item => this.getOrderKey(item));
103
+ }
104
+ else {
105
+ enumerable = enumerable.orderBy(item => this.getOrderKey(item));
106
+ }
107
+ }
108
+ let groupedEnumerable = enumerable.groupBy(item => this.getGroupField(item));
109
+ if (groupableOptions.headerOrder) {
110
+ if (groupableOptions.headerOrder === "desc") {
111
+ groupedEnumerable = groupedEnumerable.orderByDescending(g => g.key);
112
+ }
113
+ else {
114
+ groupedEnumerable = groupedEnumerable.orderBy(g => g.key);
115
+ }
116
+ }
117
+ const previousHeaders = this.#groupHeaders;
118
+ const headers = new Map();
119
+ const items = groupedEnumerable
120
+ .selectMany(g => {
121
+ const header = previousHeaders.get(g.key) ??
122
+ new ListItem({ data: g.key, header: String(g.key ?? ""), isHeader: true });
123
+ headers.set(g.key, header);
124
+ return g.source.prepend(header);
125
+ })
126
+ .toImmutableSet();
127
+ this.#groupHeaders = headers;
128
+ return items;
129
+ }, /* @ts-ignore */
130
+ ...(ngDevMode ? [{ debugName: "#arrangedItems" }] : /* istanbul ignore next */ []));
93
131
  this.#data = signal(ImmutableList.create(), /* @ts-ignore */
94
132
  ...(ngDevMode ? [{ debugName: "#data" }] : /* istanbul ignore next */ []));
95
- this.#listItems = computed(() => {
96
- const data = this.#data();
97
- return from(data)
98
- .select(item => new ListItem({ data: item, header: "" }))
99
- .toImmutableSet();
133
+ /**
134
+ * First list item for each selection key, so key lookups are O(1) instead of a scan per key.
135
+ */
136
+ this.#itemsByKey = computed(() => {
137
+ const itemsByKey = new Map();
138
+ for (const item of this.#listItems()) {
139
+ const key = this.getSelectionKey(item);
140
+ if (!itemsByKey.has(key)) {
141
+ itemsByKey.set(key, item);
142
+ }
143
+ }
144
+ return itemsByKey;
145
+ }, /* @ts-ignore */
146
+ ...(ngDevMode ? [{ debugName: "#itemsByKey" }] : /* istanbul ignore next */ []));
147
+ this.#itemsByUid = computed(() => {
148
+ const itemsByUid = new Map();
149
+ for (const item of this.#listItems()) {
150
+ itemsByUid.set(item.uid, item);
151
+ }
152
+ return itemsByUid;
100
153
  }, /* @ts-ignore */
101
- ...(ngDevMode ? [{ debugName: "#listItems" }] : /* istanbul ignore next */ []));
154
+ ...(ngDevMode ? [{ debugName: "#itemsByUid" }] : /* istanbul ignore next */ []));
155
+ /**
156
+ * Wraps the data in list items while keeping item identity across data changes:
157
+ * an item whose selection key existed before keeps its `uid`, and keeps its instance when the data
158
+ * object is also unchanged. Highlight, `aria-activedescendant` ids and rendered rows therefore survive
159
+ * a refresh (for example server-side filtering) instead of pointing at items that no longer exist.
160
+ */
161
+ this.#listItems = linkedSignal({ ...(ngDevMode ? { debugName: "#listItems" } : /* istanbul ignore next */ {}), source: () => ({ data: this.#data(), valueField: this.valueField() }),
162
+ computation: ({ data }, previous) => {
163
+ const previousByKey = new Map();
164
+ for (const item of previous?.value ?? []) {
165
+ const key = this.getSelectionKey(item);
166
+ if (!previousByKey.has(key)) {
167
+ previousByKey.set(key, item);
168
+ }
169
+ }
170
+ const usedUids = new Set();
171
+ const items = [];
172
+ for (const dataItem of data) {
173
+ const previousItem = previousByKey.get(this.getDataItemValue(dataItem));
174
+ let item;
175
+ if (previousItem && !usedUids.has(previousItem.uid)) {
176
+ item =
177
+ previousItem.data === dataItem
178
+ ? previousItem
179
+ : new ListItem({ data: dataItem, uid: previousItem.uid });
180
+ }
181
+ else {
182
+ item = new ListItem({ data: dataItem });
183
+ }
184
+ usedUids.add(item.uid);
185
+ items.push(item);
186
+ }
187
+ return toImmutableSet(items);
188
+ } });
189
+ /**
190
+ * Group header items from the last grouping pass, reused so header rows keep their uid between recomputations.
191
+ */
192
+ this.#groupHeaders = new Map();
102
193
  this.disabledBy = signal("", /* @ts-ignore */
103
194
  ...(ngDevMode ? [{ debugName: "disabledBy" }] : /* istanbul ignore next */ []));
104
195
  this.filterChange$ = new Subject();
@@ -125,26 +216,68 @@ class ListService {
125
216
  if (selected) {
126
217
  return selected;
127
218
  }
128
- return this.viewItems().firstOrDefault(i => !i.header && !this.isDisabled(i));
219
+ return this.viewItems().firstOrDefault(i => !i.isHeader && !this.isDisabled(i));
129
220
  }, /* @ts-ignore */
130
221
  ...(ngDevMode ? [{ debugName: "focusableItem" }] : /* istanbul ignore next */ []));
131
222
  this.groupBy = signal(null, /* @ts-ignore */
132
223
  ...(ngDevMode ? [{ debugName: "groupBy" }] : /* istanbul ignore next */ []));
224
+ /**
225
+ * Maps each option uid to the uid of the group header it is listed under.
226
+ */
227
+ this.groupHeaderUids = computed(() => {
228
+ const headerUids = new Map();
229
+ let headerUid = null;
230
+ for (const item of this.#arrangedItems()) {
231
+ if (item.isHeader) {
232
+ headerUid = item.uid;
233
+ }
234
+ else if (headerUid != null) {
235
+ headerUids.set(item.uid, headerUid);
236
+ }
237
+ }
238
+ return headerUids;
239
+ }, /* @ts-ignore */
240
+ ...(ngDevMode ? [{ debugName: "groupHeaderUids" }] : /* istanbul ignore next */ []));
133
241
  this.groupableOptions = signal({
134
242
  enabled: false,
135
243
  headerOrder: "asc"
136
244
  }, /* @ts-ignore */
137
245
  ...(ngDevMode ? [{ debugName: "groupableOptions" }] : /* istanbul ignore next */ []));
138
- this.highlightedItem = signal(null, /* @ts-ignore */
139
- ...(ngDevMode ? [{ debugName: "highlightedItem" }] : /* istanbul ignore next */ []));
246
+ /**
247
+ * The highlighted (active) item. When the data changes, it is re-resolved to the item with the same `uid`,
248
+ * or cleared when that item is gone.
249
+ */
250
+ this.highlightedItem = linkedSignal({ ...(ngDevMode ? { debugName: "highlightedItem" } : /* istanbul ignore next */ {}), source: this.#listItems,
251
+ computation: (_, previous) => {
252
+ const highlighted = previous?.value;
253
+ if (!highlighted || highlighted.isHeader) {
254
+ return null;
255
+ }
256
+ return this.#itemsByUid().get(highlighted.uid) ?? null;
257
+ } });
140
258
  this.itemFocusOut$ = new Subject();
141
- this.navigatedItem$ = new Subject();
259
+ /**
260
+ * Position of each option among all filtered options (not only the current page), for `aria-posinset`
261
+ * and `aria-setsize`.
262
+ */
263
+ this.itemPositions = computed(() => {
264
+ const options = this.#arrangedItems()
265
+ .where(i => !i.isHeader)
266
+ .toArray();
267
+ const positions = new Map();
268
+ options.forEach((item, index) => {
269
+ positions.set(item.uid, { position: index + 1, total: options.length });
270
+ });
271
+ return positions;
272
+ }, /* @ts-ignore */
273
+ ...(ngDevMode ? [{ debugName: "itemPositions" }] : /* istanbul ignore next */ []));
142
274
  this.navigableOptions = signal({
143
275
  enabled: false,
144
276
  mode: "select",
145
277
  wrap: false
146
278
  }, /* @ts-ignore */
147
279
  ...(ngDevMode ? [{ debugName: "navigableOptions" }] : /* istanbul ignore next */ []));
280
+ this.navigatedItem$ = new Subject();
148
281
  this.pageState = signal({ page: 1, skip: 0, take: 10 }, /* @ts-ignore */
149
282
  ...(ngDevMode ? [{ debugName: "pageState" }] : /* istanbul ignore next */ []));
150
283
  this.pageableOptions = signal({
@@ -171,9 +304,10 @@ class ListService {
171
304
  this.selectedKeysChange$ = new Subject();
172
305
  this.selectedListItems = computed(() => {
173
306
  const selectedKeys = this.selectedKeys();
307
+ const itemsByKey = this.#itemsByKey();
174
308
  return selectedKeys
175
- .select(key => this.#listItems().firstOrDefault(i => this.getSelectionKey(i) === key))
176
- .where(i => i != null)
309
+ .select(key => itemsByKey.get(key))
310
+ .where((i) => i != null)
177
311
  .toImmutableSet();
178
312
  }, /* @ts-ignore */
179
313
  ...(ngDevMode ? [{ debugName: "selectedListItems" }] : /* istanbul ignore next */ []));
@@ -182,53 +316,17 @@ class ListService {
182
316
  ...(ngDevMode ? [{ debugName: "textField" }] : /* istanbul ignore next */ []));
183
317
  this.valueField = signal("", /* @ts-ignore */
184
318
  ...(ngDevMode ? [{ debugName: "valueField" }] : /* istanbul ignore next */ []));
319
+ /**
320
+ * Visible items in display order: filtered, ordered and grouped (with header rows), but not paged.
321
+ */
185
322
  this.viewItems = computed(() => {
186
- const listItems = this.#listItems();
187
- const filterText = this.filterText();
188
- const groupableOptions = this.groupableOptions();
189
- const virtualScrollOptions = this.virtualScrollOptions();
190
- let enumerable = listItems;
191
- if (filterText) {
192
- enumerable = enumerable.where(item => this.filterItem(item, filterText));
193
- }
194
- let resultEnumerable;
195
- if (groupableOptions.enabled) {
196
- if (groupableOptions.orderBy) {
197
- if (groupableOptions.orderByDirection === "desc") {
198
- enumerable = enumerable.orderByDescending(item => this.getOrderKey(item));
199
- }
200
- else {
201
- enumerable = enumerable.orderBy(item => this.getOrderKey(item));
202
- }
203
- }
204
- let groupedEnumerable = enumerable.groupBy(item => this.getGroupField(item));
205
- if (groupableOptions.headerOrder) {
206
- if (groupableOptions.headerOrder === "desc") {
207
- groupedEnumerable = groupedEnumerable.orderByDescending(g => g.key);
208
- }
209
- else {
210
- groupedEnumerable = groupedEnumerable.orderBy(g => g.key);
211
- }
212
- }
213
- resultEnumerable = groupedEnumerable.selectMany(g => {
214
- const groupHeaderItem = new ListItem({ data: g.key, header: String(g.key) });
215
- return g.source.prepend(groupHeaderItem);
216
- });
217
- }
218
- else {
219
- resultEnumerable = enumerable;
220
- }
323
+ const arrangedItems = this.#arrangedItems();
221
324
  const pageableOptions = this.pageableOptions();
222
- if (virtualScrollOptions.enabled) {
223
- return resultEnumerable.toImmutableSet();
224
- }
225
- if (pageableOptions.enabled) {
226
- const pageState = this.pageState();
227
- const skip = pageState.skip;
228
- const take = pageState.take;
229
- return toImmutableSet(this.skipTakeWithGroups(resultEnumerable.toArray(), skip, take));
325
+ if (this.virtualScrollOptions().enabled || !pageableOptions.enabled) {
326
+ return arrangedItems;
230
327
  }
231
- return resultEnumerable.toImmutableSet();
328
+ const pageState = this.pageState();
329
+ return toImmutableSet(this.skipTakeWithGroups(arrangedItems.toArray(), pageState.skip, pageState.take));
232
330
  }, /* @ts-ignore */
233
331
  ...(ngDevMode ? [{ debugName: "viewItems" }] : /* istanbul ignore next */ []));
234
332
  this.virtualScrollOptions = signal({
@@ -236,41 +334,25 @@ class ListService {
236
334
  height: 28
237
335
  }, /* @ts-ignore */
238
336
  ...(ngDevMode ? [{ debugName: "virtualScrollOptions" }] : /* istanbul ignore next */ []));
239
- this.itemPositions = computed(() => {
240
- const viewItems = this.viewItems();
241
- const nonHeaderItems = viewItems.where(i => !i.header).toArray();
242
- const positions = new Map();
243
- nonHeaderItems.forEach((item, index) => {
244
- positions.set(item.uid, { position: index + 1, total: nonHeaderItems.length });
245
- });
246
- return positions;
247
- }, /* @ts-ignore */
248
- ...(ngDevMode ? [{ debugName: "itemPositions" }] : /* istanbul ignore next */ []));
249
337
  }
338
+ #arrangedItems;
250
339
  #data;
340
+ /**
341
+ * First list item for each selection key, so key lookups are O(1) instead of a scan per key.
342
+ */
343
+ #itemsByKey;
344
+ #itemsByUid;
345
+ /**
346
+ * Wraps the data in list items while keeping item identity across data changes:
347
+ * an item whose selection key existed before keeps its `uid`, and keeps its instance when the data
348
+ * object is also unchanged. Highlight, `aria-activedescendant` ids and rendered rows therefore survive
349
+ * a refresh (for example server-side filtering) instead of pointing at items that no longer exist.
350
+ */
251
351
  #listItems;
252
- getItemPosition(item) {
253
- return this.itemPositions().get(item.uid) ?? null;
254
- }
255
352
  /**
256
- * Resolves the selection value of a data item through `valueField`.
257
- *
258
- * When no `valueField` is configured, the data item itself is the value.
259
- * When `valueField` is a string, the item property is read with a nullish fallback to the item.
260
- * When `valueField` is a function, its result is used as-is.
261
- *
262
- * Falsy results such as `0`, `false`, and `""` are valid values and do not fall back to the item.
353
+ * Group header items from the last grouping pass, reused so header rows keep their uid between recomputations.
263
354
  */
264
- getDataItemValue(dataItem) {
265
- const valueField = this.valueField();
266
- if (!valueField) {
267
- return dataItem;
268
- }
269
- if (typeof valueField === "string") {
270
- return dataItem?.[valueField] ?? dataItem;
271
- }
272
- return valueField(dataItem);
273
- }
355
+ #groupHeaders;
274
356
  clearFilter() {
275
357
  this.filterText.set("");
276
358
  }
@@ -281,12 +363,18 @@ class ListService {
281
363
  this.selectedKeys.update(set => set.clear());
282
364
  this.highlightedItem.set(null);
283
365
  }
366
+ /**
367
+ * Finds the next item whose text starts with the typeahead buffer, starting after the active item.
368
+ * Group headers and disabled items are never matched.
369
+ */
284
370
  cycleThroughMatchedItems(buffer) {
285
371
  const activeItem = this.highlightedItem() || this.selectedListItems().lastOrDefault();
286
- const viewItems = this.viewItems();
287
- return cycleThroughMatchedItems(viewItems, activeItem, item => {
288
- const text = this.getItemText(item);
289
- return text.toLowerCase().startsWith(buffer.toLowerCase());
372
+ const prefix = buffer.toLowerCase();
373
+ return cycleThroughMatchedItems(this.viewItems(), activeItem ?? null, item => {
374
+ if (item.isHeader || this.isDisabled(item)) {
375
+ return false;
376
+ }
377
+ return this.getItemText(item).toLowerCase().startsWith(prefix);
290
378
  });
291
379
  }
292
380
  deselectItems(item) {
@@ -295,6 +383,25 @@ class ListService {
295
383
  .toImmutableSet();
296
384
  this.selectedKeys.update(set => set.removeAll(keys));
297
385
  }
386
+ /**
387
+ * Resolves the selection value of a data item through `valueField`.
388
+ *
389
+ * When no `valueField` is configured, the data item itself is the value.
390
+ * When `valueField` is a string, the item property is read with a nullish fallback to the item.
391
+ * When `valueField` is a function, its result is used as-is.
392
+ *
393
+ * Falsy results such as `0`, `false`, and `""` are valid values and do not fall back to the item.
394
+ */
395
+ getDataItemValue(dataItem) {
396
+ const valueField = this.valueField();
397
+ if (!valueField) {
398
+ return dataItem;
399
+ }
400
+ if (typeof valueField === "string") {
401
+ return dataItem?.[valueField] ?? dataItem;
402
+ }
403
+ return valueField(dataItem);
404
+ }
298
405
  getFilterDebounceDuration() {
299
406
  const filterableOptions = this.filterableOptions();
300
407
  return filterableOptions.enabled ? filterableOptions.debounce : 0;
@@ -309,33 +416,54 @@ class ListService {
309
416
  }
310
417
  return groupSelector(item.data);
311
418
  }
419
+ /**
420
+ * Returns the uid of the group header an item is rendered under, or `null` when the list is not grouped.
421
+ */
422
+ getGroupHeaderUid(item) {
423
+ return this.groupHeaderUids().get(item.uid) ?? null;
424
+ }
425
+ getItemPosition(item) {
426
+ return this.itemPositions().get(item.uid) ?? null;
427
+ }
428
+ /**
429
+ * Resolves the display text of an item through `textField`.
430
+ * Non-string field values (numbers, dates, …) are converted with `String()`, and nullish values become `""`.
431
+ */
312
432
  getItemText(item) {
313
433
  const textField = this.textField();
434
+ let text;
314
435
  if (!textField) {
315
- return item.data?.toString() ?? "";
436
+ text = item.data;
316
437
  }
317
- if (typeof textField === "string") {
318
- return item.data?.[textField] ?? "";
438
+ else if (typeof textField === "string") {
439
+ text = item.data?.[textField];
319
440
  }
320
- return textField(item.data);
441
+ else {
442
+ text = textField(item.data);
443
+ }
444
+ return text == null ? "" : String(text);
321
445
  }
446
+ /**
447
+ * Finds the item that typed text should resolve to: an exact text match first (respecting `caseSensitive`),
448
+ * otherwise the first item accepted by the filter operator. Group headers and disabled items are skipped.
449
+ */
322
450
  getMatchingFilteredItem(text) {
323
451
  const filterableOptions = this.filterableOptions();
324
452
  const caseSensitive = filterableOptions.enabled ? filterableOptions.caseSensitive : false;
325
- const viewItems = this.viewItems();
326
- const allMatchingItems = viewItems.where(i => this.filterItem(i, text));
327
- const fullMatchingItem = viewItems.firstOrDefault(i => {
328
- if (!caseSensitive) {
329
- return this.getItemText(i) === text;
330
- }
331
- return this.getItemText(i).toLowerCase() === text.toLowerCase();
453
+ const candidates = this.viewItems().where(i => !i.isHeader && !this.isDisabled(i));
454
+ const fullMatchingItem = candidates.firstOrDefault(i => {
455
+ const itemText = this.getItemText(i);
456
+ return caseSensitive ? itemText === text : itemText.toLowerCase() === text.toLowerCase();
332
457
  });
333
458
  if (fullMatchingItem) {
334
459
  return fullMatchingItem;
335
460
  }
336
- return allMatchingItems.firstOrDefault();
461
+ return candidates.firstOrDefault(i => this.filterItem(i, text));
337
462
  }
338
463
  isDisabled(item) {
464
+ if (item.isHeader) {
465
+ return false;
466
+ }
339
467
  const disabledBy = this.disabledBy();
340
468
  if (typeof disabledBy === "string") {
341
469
  return item.data?.[disabledBy] ?? false;
@@ -365,7 +493,7 @@ class ListService {
365
493
  return null;
366
494
  }
367
495
  const viewItems = this.viewItems()
368
- .where(i => !i.header && !this.isDisabled(i))
496
+ .where(i => !i.isHeader && !this.isDisabled(i))
369
497
  .toImmutableSet();
370
498
  if (viewItems.isEmpty()) {
371
499
  return null;
@@ -393,6 +521,26 @@ class ListService {
393
521
  scrollToItem(item, focus) {
394
522
  this.scrollToItem$.next({ item, focus });
395
523
  }
524
+ /**
525
+ * Selects every enabled item currently in view, keeping existing selections. Only applies in multiple mode.
526
+ * @returns whether the selection changed.
527
+ */
528
+ selectAllItems() {
529
+ const options = this.selectableOptions();
530
+ if (!options.enabled || options.mode !== "multiple") {
531
+ return false;
532
+ }
533
+ const keys = this.viewItems()
534
+ .where(i => !i.isHeader && !this.isDisabled(i))
535
+ .select(i => this.getSelectionKey(i))
536
+ .toArray();
537
+ const selectedKeys = this.selectedKeys();
538
+ if (keys.every(key => selectedKeys.contains(key))) {
539
+ return false;
540
+ }
541
+ this.selectedKeys.update(set => set.addAll(keys));
542
+ return true;
543
+ }
396
544
  selectItem(item, scroll = false, focus = false) {
397
545
  const key = this.getSelectionKey(item);
398
546
  const options = this.selectableOptions();
@@ -496,6 +644,13 @@ class ListService {
496
644
  return false;
497
645
  }
498
646
  }
647
+ /**
648
+ * Returns the item when it is one of the navigable items, otherwise `null`.
649
+ * An anchor that was filtered out or removed must not be navigated from.
650
+ */
651
+ getItemInView(viewItems, item) {
652
+ return item != null && viewItems.contains(item) ? item : null;
653
+ }
499
654
  getOrderKey(item) {
500
655
  const orderBy = this.groupableOptions().orderBy;
501
656
  if (orderBy) {
@@ -524,16 +679,34 @@ class ListService {
524
679
  getSelectionKey(item) {
525
680
  return this.getDataItemValue(item.data);
526
681
  }
682
+ navigateFirstForSingleSelection(viewItems, mode) {
683
+ const firstItem = viewItems.first();
684
+ if (mode === "select") {
685
+ if (!this.isSelected(firstItem)) {
686
+ this.selectItem(firstItem);
687
+ }
688
+ this.highlightedItem.set(null);
689
+ }
690
+ else {
691
+ this.highlightedItem.set(firstItem);
692
+ }
693
+ return firstItem;
694
+ }
527
695
  navigateForMultipleSelection(viewItems, direction) {
528
696
  const selectedKeys = this.selectedKeys();
529
697
  const firstItem = viewItems.first();
530
- if (selectedKeys.isEmpty() && !this.highlightedItem()) {
698
+ if (direction === "first" || direction === "last") {
699
+ const edgeItem = direction === "first" ? firstItem : viewItems.last();
700
+ this.highlightedItem.set(edgeItem);
701
+ return edgeItem;
702
+ }
703
+ const lastHighlightedItem = this.getItemInView(viewItems, this.highlightedItem());
704
+ if (selectedKeys.isEmpty() && !lastHighlightedItem) {
531
705
  this.highlightedItem.set(firstItem);
532
706
  return firstItem;
533
707
  }
534
708
  const firstSelectedItem = viewItems.firstOrDefault(i => selectedKeys.contains(this.getSelectionKey(i)));
535
709
  const lastSelectedItem = viewItems.lastOrDefault(i => selectedKeys.contains(this.getSelectionKey(i)));
536
- const lastHighlightedItem = this.highlightedItem();
537
710
  if (direction === "next") {
538
711
  const navigationItem = lastHighlightedItem ?? lastSelectedItem ?? firstItem;
539
712
  let nextItem = viewItems
@@ -564,21 +737,14 @@ class ListService {
564
737
  }
565
738
  return null;
566
739
  }
567
- navigateFirstForSingleSelection(viewItems, mode) {
568
- const firstItem = viewItems.first();
569
- if (mode === "select") {
570
- this.selectItem(firstItem);
571
- this.highlightedItem.set(null);
572
- }
573
- else {
574
- this.highlightedItem.set(firstItem);
575
- }
576
- return firstItem;
577
- }
578
740
  navigateForSingleSelection(viewItems, direction, mode) {
579
741
  const selectedKeys = this.selectedKeys();
580
742
  const firstItem = viewItems.first();
581
- if (selectedKeys.isEmpty() && !this.highlightedItem()) {
743
+ const lastHighlightedItem = this.getItemInView(viewItems, this.highlightedItem());
744
+ if (selectedKeys.isEmpty() && !lastHighlightedItem) {
745
+ if (direction === "last") {
746
+ return this.navigateLastForSingleSelection(viewItems, mode);
747
+ }
582
748
  if (mode === "select") {
583
749
  this.selectItem(firstItem);
584
750
  }
@@ -587,15 +753,13 @@ class ListService {
587
753
  }
588
754
  return firstItem;
589
755
  }
590
- else if (selectedKeys.isEmpty() && this.highlightedItem()) {
591
- const highlightedItem = this.highlightedItem();
756
+ else if (selectedKeys.isEmpty() && lastHighlightedItem) {
592
757
  if (mode === "select") {
593
- this.selectItem(highlightedItem);
594
- return highlightedItem;
758
+ this.selectItem(lastHighlightedItem);
759
+ return lastHighlightedItem;
595
760
  }
596
761
  }
597
762
  const selectedItem = viewItems.lastOrDefault(i => selectedKeys.contains(this.getSelectionKey(i)));
598
- const lastHighlightedItem = this.highlightedItem();
599
763
  const navigationItem = lastHighlightedItem ?? selectedItem ?? firstItem;
600
764
  if (direction === "next") {
601
765
  return this.navigateNextForSingleSelection(viewItems, navigationItem, mode, firstItem);
@@ -614,7 +778,9 @@ class ListService {
614
778
  navigateLastForSingleSelection(viewItems, mode) {
615
779
  const lastItem = viewItems.last();
616
780
  if (mode === "select") {
617
- this.selectItem(lastItem);
781
+ if (!this.isSelected(lastItem)) {
782
+ this.selectItem(lastItem);
783
+ }
618
784
  this.highlightedItem.set(null);
619
785
  }
620
786
  else {
@@ -675,7 +841,7 @@ class ListService {
675
841
  let capturing = false;
676
842
  const result = [];
677
843
  for (const item of items) {
678
- if (item.header) {
844
+ if (item.isHeader) {
679
845
  // Include group headers when we're capturing
680
846
  if (capturing) {
681
847
  result.push(item);
@@ -693,7 +859,7 @@ class ListService {
693
859
  // Find the group header for this item
694
860
  const itemIndex = items.indexOf(item);
695
861
  for (let i = itemIndex - 1; i >= 0; i--) {
696
- if (items[i].header) {
862
+ if (items[i].isHeader) {
697
863
  result.push(items[i]);
698
864
  break;
699
865
  }
@@ -710,7 +876,7 @@ class ListService {
710
876
  }
711
877
  // Remove trailing group headers if they are at the end
712
878
  let lastIndex = result.length - 1;
713
- while (lastIndex >= 0 && result[lastIndex].header) {
879
+ while (lastIndex >= 0 && result[lastIndex].isHeader) {
714
880
  result.pop();
715
881
  lastIndex--;
716
882
  }
@@ -780,6 +946,8 @@ class ListItemDirective {
780
946
  this.#listService = inject((ListService));
781
947
  this.disabled = computed(() => this.#listService.isDisabled(this.item()), /* @ts-ignore */
782
948
  ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
949
+ this.groupHeaderUid = computed(() => this.#listService.getGroupHeaderUid(this.item()), /* @ts-ignore */
950
+ ...(ngDevMode ? [{ debugName: "groupHeaderUid" }] : /* istanbul ignore next */ []));
783
951
  this.highlighted = computed(() => this.#listService.isHighlighted(this.item()), /* @ts-ignore */
784
952
  ...(ngDevMode ? [{ debugName: "highlighted" }] : /* istanbul ignore next */ []));
785
953
  this.position = computed(() => {
@@ -819,15 +987,14 @@ class ListItemDirective {
819
987
  .subscribe(() => this.#listService.itemFocusOut$.next(this.item()));
820
988
  }
821
989
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ListItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
822
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.6", type: ListItemDirective, isStandalone: true, selector: "li[monaListItem]", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "attr.aria-current": "highlighted()", "attr.aria-disabled": "disabled()", "attr.aria-posinset": "position()", "attr.aria-selected": "selected()", "attr.aria-setsize": "setSize()", "attr.data-disabled": "disabled()", "attr.data-highlighted": "highlighted()", "attr.data-selected": "selected()", "attr.role": "'option'", "attr.tabindex": "rovingTabIndex()", "class": "textClasses()" } }, ngImport: i0 }); }
990
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.6", type: ListItemDirective, isStandalone: true, selector: "li[monaListItem]", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "attr.aria-describedby": "groupHeaderUid()", "attr.aria-disabled": "disabled()", "attr.aria-posinset": "position()", "attr.aria-selected": "selected()", "attr.aria-setsize": "setSize()", "attr.data-disabled": "disabled()", "attr.data-highlighted": "highlighted()", "attr.data-selected": "selected()", "attr.role": "'option'", "attr.tabindex": "rovingTabIndex()", "class": "textClasses()" } }, ngImport: i0 }); }
823
991
  }
824
992
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ListItemDirective, decorators: [{
825
993
  type: Directive,
826
994
  args: [{
827
995
  selector: "li[monaListItem]",
828
- standalone: true,
829
996
  host: {
830
- "[attr.aria-current]": "highlighted()",
997
+ "[attr.aria-describedby]": "groupHeaderUid()",
831
998
  "[attr.aria-disabled]": "disabled()",
832
999
  "[attr.aria-posinset]": "position()",
833
1000
  "[attr.aria-selected]": "selected()",
@@ -849,8 +1016,7 @@ class ListNoDataTemplateDirective {
849
1016
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ListNoDataTemplateDirective, decorators: [{
850
1017
  type: Directive,
851
1018
  args: [{
852
- selector: "ng-template[monaListNoDataTemplate]",
853
- standalone: true
1019
+ selector: "ng-template[monaListNoDataTemplate]"
854
1020
  }]
855
1021
  }] });
856
1022
 
@@ -898,13 +1064,13 @@ class ListItemComponent {
898
1064
  if (item == null) {
899
1065
  return "";
900
1066
  }
901
- if (item.header) {
1067
+ if (item.isHeader) {
902
1068
  return item.header;
903
1069
  }
904
1070
  return this.listService.getItemText(item);
905
1071
  }, /* @ts-ignore */
906
1072
  ...(ngDevMode ? [{ debugName: "itemText" }] : /* istanbul ignore next */ []));
907
- this.isHeader = computed(() => this.item()?.header ?? false, /* @ts-ignore */
1073
+ this.isHeader = computed(() => this.item()?.isHeader ?? false, /* @ts-ignore */
908
1074
  ...(ngDevMode ? [{ debugName: "isHeader" }] : /* istanbul ignore next */ []));
909
1075
  this.listService = inject(ListService);
910
1076
  this.textClasses = computed(() => {
@@ -917,42 +1083,53 @@ class ListItemComponent {
917
1083
  return ``; // TODO
918
1084
  }, /* @ts-ignore */
919
1085
  ...(ngDevMode ? [{ debugName: "textClasses" }] : /* istanbul ignore next */ []));
920
- this.checked = output();
921
1086
  this.item = input.required(/* @ts-ignore */
922
1087
  ...(ngDevMode ? [{ debugName: "item" }] : /* istanbul ignore next */ []));
923
1088
  this.template = input(/* @ts-ignore */
924
1089
  ...(ngDevMode ? [undefined, { debugName: "template" }] : /* istanbul ignore next */ []));
925
1090
  }
926
- onCheckboxChange(checked) {
927
- this.checked.emit(checked);
928
- }
929
1091
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
930
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: ListItemComponent, isStandalone: true, selector: "mona-list-item", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null }, template: { classPropertyName: "template", publicName: "template", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checked: "checked" }, host: { properties: { "class": "baseClass()" } }, ngImport: i0, template: "@if (checkboxes()) {\n <input\n type=\"checkbox\"\n [ngModel]=\"itemChecked()\"\n monaCheckbox\n class=\"aspect-square\"\n (click)=\"$event.stopPropagation()\"\n (ngModelChange)=\"onCheckboxChange($event)\"\n (change)=\"$event.preventDefault()\" />\n}\n\n@if (template()) {\n <ng-container [ngTemplateOutlet]=\"template()\" [ngTemplateOutletContext]=\"{ $implicit: dataItem() }\"></ng-container>\n} @else {\n <span [class]=\"textClasses()\">{{ itemText() }}</span>\n}\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: CheckboxDirective, selector: "input[type='checkbox'][monaCheckbox]", inputs: ["invalid", "rounded", "touched", "class"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox]:not([ngNoCva])[formControlName],input[type=checkbox]:not([ngNoCva])[formControl],input[type=checkbox]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1092
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: ListItemComponent, isStandalone: true, selector: "mona-list-item", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null }, template: { classPropertyName: "template", publicName: "template", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "baseClass()" } }, ngImport: i0, template: "@if (checkboxes()) {\n <!-- Visual only: the option row owns selection, so the checkbox is inert (not focusable, clicks reach the row). -->\n <input type=\"checkbox\" [checked]=\"itemChecked()\" monaCheckbox class=\"aspect-square\" inert aria-hidden=\"true\" />\n}\n\n@if (template()) {\n <ng-container [ngTemplateOutlet]=\"template()\" [ngTemplateOutletContext]=\"{ $implicit: dataItem() }\"></ng-container>\n} @else {\n <span [class]=\"textClasses()\">{{ itemText() }}</span>\n}\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: CheckboxDirective, selector: "input[type='checkbox'][monaCheckbox]", inputs: ["invalid", "rounded", "touched", "class"] }] }); }
931
1093
  }
932
1094
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ListItemComponent, decorators: [{
933
1095
  type: Component,
934
- args: [{ selector: "mona-list-item", imports: [NgTemplateOutlet, CheckboxDirective, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, host: {
1096
+ args: [{ selector: "mona-list-item", imports: [NgTemplateOutlet, CheckboxDirective], host: {
935
1097
  "[class]": "baseClass()"
936
- }, template: "@if (checkboxes()) {\n <input\n type=\"checkbox\"\n [ngModel]=\"itemChecked()\"\n monaCheckbox\n class=\"aspect-square\"\n (click)=\"$event.stopPropagation()\"\n (ngModelChange)=\"onCheckboxChange($event)\"\n (change)=\"$event.preventDefault()\" />\n}\n\n@if (template()) {\n <ng-container [ngTemplateOutlet]=\"template()\" [ngTemplateOutletContext]=\"{ $implicit: dataItem() }\"></ng-container>\n} @else {\n <span [class]=\"textClasses()\">{{ itemText() }}</span>\n}\n" }]
937
- }], propDecorators: { checked: [{ type: i0.Output, args: ["checked"] }], item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: true }] }], template: [{ type: i0.Input, args: [{ isSignal: true, alias: "template", required: false }] }] } });
1098
+ }, template: "@if (checkboxes()) {\n <!-- Visual only: the option row owns selection, so the checkbox is inert (not focusable, clicks reach the row). -->\n <input type=\"checkbox\" [checked]=\"itemChecked()\" monaCheckbox class=\"aspect-square\" inert aria-hidden=\"true\" />\n}\n\n@if (template()) {\n <ng-container [ngTemplateOutlet]=\"template()\" [ngTemplateOutletContext]=\"{ $implicit: dataItem() }\"></ng-container>\n} @else {\n <span [class]=\"textClasses()\">{{ itemText() }}</span>\n}\n" }]
1099
+ }], propDecorators: { item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: true }] }], template: [{ type: i0.Input, args: [{ isSignal: true, alias: "template", required: false }] }] } });
938
1100
 
939
1101
  class ListComponent {
940
1102
  #destroyRef;
1103
+ #generatedListId;
941
1104
  #hostElementRef;
942
1105
  #i18n;
943
1106
  #typeaheadKey$;
944
1107
  constructor() {
945
1108
  this.#destroyRef = inject(DestroyRef);
1109
+ this.#generatedListId = createElementControlId();
946
1110
  this.#hostElementRef = inject(ElementRef);
947
1111
  this.#i18n = inject(MonaI18nService);
948
1112
  this.#typeaheadKey$ = new Subject();
949
1113
  this.filterInput = viewChild(FilterInputComponent, /* @ts-ignore */
950
1114
  ...(ngDevMode ? [{ debugName: "filterInput" }] : /* istanbul ignore next */ []));
1115
+ /**
1116
+ * Id of the highlighted option, used as `aria-activedescendant` by the filter input.
1117
+ */
1118
+ this.activeDescendant = computed(() => {
1119
+ const item = this.listService.highlightedItem();
1120
+ return item && !item.isHeader ? item.uid : null;
1121
+ }, /* @ts-ignore */
1122
+ ...(ngDevMode ? [{ debugName: "activeDescendant" }] : /* istanbul ignore next */ []));
951
1123
  this.classes = computed(() => {
952
1124
  const classes = listThemeVariants();
953
1125
  return twMerge(classes);
954
1126
  }, /* @ts-ignore */
955
1127
  ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
1128
+ /**
1129
+ * The `listId` input, or a generated id so the listbox can always be referenced by `aria-controls`.
1130
+ */
1131
+ this.effectiveListId = computed(() => this.listId() || this.#generatedListId, /* @ts-ignore */
1132
+ ...(ngDevMode ? [{ debugName: "effectiveListId" }] : /* istanbul ignore next */ []));
956
1133
  this.footerTemplate = contentChild(ListFooterTemplateDirective, { ...(ngDevMode ? { debugName: "footerTemplate" } : /* istanbul ignore next */ {}), read: TemplateRef });
957
1134
  this.groupHeaderClasses = computed(() => {
958
1135
  const listItemClass = this.listItemClass();
@@ -1027,7 +1204,7 @@ class ListComponent {
1027
1204
  const items = this.listService.viewItems().size();
1028
1205
  const headerItems = this.listService
1029
1206
  .viewItems()
1030
- .where(i => !!i.header)
1207
+ .where(i => i.isHeader)
1031
1208
  .count();
1032
1209
  const itemHeight = this.listService.virtualScrollOptions().height ?? 28;
1033
1210
  const height = items * itemHeight + headerItems * 2;
@@ -1098,22 +1275,20 @@ class ListComponent {
1098
1275
  this.listService.setFilter(event.filter);
1099
1276
  }
1100
1277
  }
1101
- onListItemCheck(item) {
1102
- this.toggleItem(item);
1103
- }
1104
1278
  onListItemClick(item) {
1105
1279
  this.toggleItem(item);
1106
1280
  }
1281
+ trackByUid(_, item) {
1282
+ return item.uid;
1283
+ }
1107
1284
  cycleThroughMatchedItems(buffer) {
1108
- const nextItem = cycleThroughMatchedItems(this.listService.viewItems(), this.listService.highlightedItem(), item => {
1109
- const text = this.listService.getItemText(item);
1110
- return text.toLowerCase().startsWith(buffer.toLowerCase());
1111
- });
1285
+ const nextItem = this.listService.cycleThroughMatchedItems(buffer);
1112
1286
  if (nextItem) {
1113
1287
  const navigationMode = this.listService.navigableOptions().mode;
1114
1288
  this.listService.highlightedItem.set(nextItem);
1115
- if (navigationMode === "select") {
1289
+ if (navigationMode === "select" && this.listService.selectableOptions().enabled) {
1116
1290
  this.selectItem(nextItem);
1291
+ this.itemSelect.emit({ item: nextItem, source: { via: "keyboard", key: buffer } });
1117
1292
  }
1118
1293
  this.scrollToItem(nextItem, true, "instant");
1119
1294
  }
@@ -1122,7 +1297,33 @@ class ListComponent {
1122
1297
  if (this.listService.navigableOptions().enabled) {
1123
1298
  this.listService.highlightedItem.set(item);
1124
1299
  }
1125
- this.scrollToItem(item, true);
1300
+ this.scrollToItem(item, this.filterInput() == null);
1301
+ }
1302
+ /**
1303
+ * Handles multiple-selection shortcuts from the WAI-ARIA listbox pattern:
1304
+ * Space toggles the highlighted option and Ctrl/Cmd+A selects every enabled option.
1305
+ * @returns whether the event was handled.
1306
+ */
1307
+ handleMultipleSelectionKeys(event) {
1308
+ const selectableOptions = this.listService.selectableOptions();
1309
+ if (!selectableOptions.enabled || selectableOptions.mode !== "multiple") {
1310
+ return false;
1311
+ }
1312
+ if (event.key === " " && !event.ctrlKey && !event.metaKey && !event.altKey) {
1313
+ const highlightedItem = this.listService.highlightedItem();
1314
+ if (highlightedItem && !this.listService.isDisabled(highlightedItem)) {
1315
+ this.selectItem(highlightedItem);
1316
+ this.itemSelect.emit({ item: highlightedItem, source: { via: "keyboard", key: event.key } });
1317
+ }
1318
+ return true;
1319
+ }
1320
+ if ((event.ctrlKey || event.metaKey) && !event.altKey && event.key.toLowerCase() === "a") {
1321
+ if (this.listService.selectAllItems()) {
1322
+ this.listService.selectedKeysChange$.next(this.listService.selectedKeys().toArray());
1323
+ }
1324
+ return true;
1325
+ }
1326
+ return false;
1126
1327
  }
1127
1328
  handleNavigation(key) {
1128
1329
  const direction = getListNavigationDirection(key);
@@ -1136,7 +1337,9 @@ class ListComponent {
1136
1337
  const navigableOptions = this.listService.navigableOptions();
1137
1338
  const selectableOptions = this.listService.selectableOptions();
1138
1339
  const previousSelectedItems = this.listService.selectedListItems();
1139
- const item = this.listService.navigate(direction, navigableOptions.mode, true);
1340
+ const filterInput = this.filterInput();
1341
+ // With a filter input, focus stays in the input and the active option is conveyed by aria-activedescendant.
1342
+ const item = this.listService.navigate(direction, navigableOptions.mode, filterInput == null);
1140
1343
  if (item && navigableOptions.mode === "select" && selectableOptions.enabled) {
1141
1344
  this.itemSelect.emit({ item, source: { via: "keyboard", key } });
1142
1345
  if (previousSelectedItems.contains(item)) {
@@ -1144,7 +1347,6 @@ class ListComponent {
1144
1347
  }
1145
1348
  this.listService.selectedKeysChange$.next(this.listService.selectedKeys().toArray());
1146
1349
  }
1147
- const filterInput = this.filterInput();
1148
1350
  if (filterInput) {
1149
1351
  filterInput.focus();
1150
1352
  }
@@ -1181,9 +1383,10 @@ class ListComponent {
1181
1383
  scrollToItem(item, focus, behavior = "auto") {
1182
1384
  const element = this.#hostElementRef.nativeElement.querySelector(`[data-uid="${item.uid}"]`);
1183
1385
  if (element) {
1184
- element.scrollIntoView({ block: "center", behavior });
1386
+ // "nearest" avoids scrolling the page when the option is already visible.
1387
+ element.scrollIntoView({ block: "nearest", behavior });
1185
1388
  if (focus) {
1186
- element.focus();
1389
+ element.focus({ preventScroll: true });
1187
1390
  }
1188
1391
  }
1189
1392
  else if (this.listService.virtualScrollOptions().enabled) {
@@ -1195,7 +1398,7 @@ class ListComponent {
1195
1398
  this.virtualScrollViewport()?.scrollToOffset(offset, behavior);
1196
1399
  const scrolledElement = this.#hostElementRef.nativeElement.querySelector(`[data-uid="${item.uid}"]`);
1197
1400
  if (scrolledElement && focus) {
1198
- scrolledElement.focus();
1401
+ scrolledElement.focus({ preventScroll: true });
1199
1402
  }
1200
1403
  });
1201
1404
  }
@@ -1226,7 +1429,7 @@ class ListComponent {
1226
1429
  }
1227
1430
  const firstItem = this.listService
1228
1431
  .viewItems()
1229
- .where(i => !i.header && !this.listService.isDisabled(i))
1432
+ .where(i => !i.isHeader && !this.listService.isDisabled(i))
1230
1433
  .firstOrDefault();
1231
1434
  if (firstItem) {
1232
1435
  this.focusToItem(firstItem);
@@ -1234,7 +1437,7 @@ class ListComponent {
1234
1437
  }
1235
1438
  setKeyboardEvents() {
1236
1439
  fromEvent(this.#hostElementRef.nativeElement, "keydown")
1237
- .pipe(takeUntilDestroyed(this.#destroyRef), filter(event => event.key === "Enter" || event.key === "NumPadEnter"))
1440
+ .pipe(takeUntilDestroyed(this.#destroyRef), filter(event => event.key === "Enter"), tap(event => event.preventDefault()))
1238
1441
  .subscribe((event) => {
1239
1442
  const highlightedItem = this.listService.highlightedItem();
1240
1443
  const selectedItem = this.listService.selectedListItems().firstOrDefault();
@@ -1251,18 +1454,21 @@ class ListComponent {
1251
1454
  }
1252
1455
  setNavigationEvents() {
1253
1456
  fromEvent(this.#hostElementRef.nativeElement, "keydown")
1254
- .pipe(takeUntilDestroyed(this.#destroyRef), filter(event => event.key !== "Tab"), filter(() => this.listService.navigableOptions().enabled), tap(event => {
1255
- const navigableOptions = this.listService.navigableOptions();
1256
- if (navigableOptions.enabled) {
1457
+ .pipe(takeUntilDestroyed(this.#destroyRef), filter(() => this.listService.navigableOptions().enabled))
1458
+ .subscribe(event => {
1459
+ if (this.handleMultipleSelectionKeys(event)) {
1257
1460
  event.preventDefault();
1461
+ return;
1258
1462
  }
1259
- }))
1260
- .subscribe(event => {
1261
- if (isTypeaheadKey(event.key)) {
1463
+ if (isTypeaheadKey(event.key, event)) {
1262
1464
  event.preventDefault();
1263
1465
  this.#typeaheadKey$.next(event.key);
1264
1466
  return;
1265
1467
  }
1468
+ if (getListNavigationDirection(event.key) == null) {
1469
+ return;
1470
+ }
1471
+ event.preventDefault();
1266
1472
  this.#typeaheadKey$.next("");
1267
1473
  this.handleNavigation(event.key);
1268
1474
  });
@@ -1311,7 +1517,7 @@ class ListComponent {
1311
1517
  this.itemSelect.emit({ item, source: { via: "mouse" } });
1312
1518
  }
1313
1519
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1314
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: ListComponent, isStandalone: true, selector: "mona-list", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, focusOnMount: { classPropertyName: "focusOnMount", publicName: "focusOnMount", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, listClass: { classPropertyName: "listClass", publicName: "listClass", isSignal: true, isRequired: false, transformFunction: null }, listId: { classPropertyName: "listId", publicName: "listId", isSignal: true, isRequired: false, transformFunction: null }, listItemClass: { classPropertyName: "listItemClass", publicName: "listItemClass", isSignal: true, isRequired: false, transformFunction: null }, listItemStyle: { classPropertyName: "listItemStyle", publicName: "listItemStyle", isSignal: true, isRequired: false, transformFunction: null }, listStyle: { classPropertyName: "listStyle", publicName: "listStyle", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, maxWidth: { classPropertyName: "maxWidth", publicName: "maxWidth", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, textField: { classPropertyName: "textField", publicName: "textField", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemSelect: "itemSelect" }, host: { properties: { "attr.tabindex": "hostTabIndex()", "class": "classes()", "style.height": "listHeight()", "style.max-height": "listMaxHeight()", "style.max-width": "listMaxWidth()", "style.width": "listWidth()" } }, queries: [{ propertyName: "footerTemplate", first: true, predicate: ListFooterTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "groupHeaderTemplate", first: true, predicate: ListGroupHeaderTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "headerTemplate", first: true, predicate: ListHeaderTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "itemTemplate", first: true, predicate: ListItemTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "noDataTemplate", first: true, predicate: ListNoDataTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }], viewQueries: [{ propertyName: "filterInput", first: true, predicate: FilterInputComponent, descendants: true, isSignal: true }, { propertyName: "virtualScrollViewport", first: true, predicate: CdkVirtualScrollViewport, descendants: true, isSignal: true }], ngImport: i0, template: "@if (headerTemplate()) {\n <ng-container [ngTemplateOutlet]=\"headerTemplate()\"></ng-container>\n}\n@if (listService.filterableOptions().enabled && listService.filterInputVisible()) {\n <div class=\"p-2\">\n <mona-filter-input\n [filter]=\"listService.filterText()\"\n [debounce]=\"listService.filterableOptions().debounce\"\n [placeholder]=\"listService.filterPlaceholder()\"\n [size]=\"size()\"\n (filterChange)=\"onFilterChange($event)\"></mona-filter-input>\n </div>\n}\n@if (listService.virtualScrollOptions().enabled) {\n @if (listService.viewItems().length !== 0) {\n <cdk-virtual-scroll-viewport\n [itemSize]=\"listService.virtualScrollOptions().height\"\n [style.height]=\"viewportHeight()\">\n <ul\n [class]=\"innerListClasses()\"\n [style]=\"listStyle()\"\n [tabIndex]=\"-1\"\n [id]=\"listId()\"\n role=\"listbox\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-multiselectable]=\"listService.selectableOptions().mode === 'multiple' || null\">\n <ng-container *cdkVirtualFor=\"let item of listService.viewItems()\">\n <ng-container\n [ngTemplateOutlet]=\"listItemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"></ng-container>\n </ng-container>\n </ul>\n </cdk-virtual-scroll-viewport>\n } @else if (noDataTemplate()) {\n <ng-container [ngTemplateOutlet]=\"noDataTemplate()\"></ng-container>\n } @else {\n <mona-placeholder [text]=\"messages().noData\" class=\"w-full h-full flex items-center justify-center\"></mona-placeholder>\n }\n} @else {\n @if (listService.viewItems().length !== 0) {\n <ul\n [class]=\"innerListClasses()\"\n [style]=\"listStyle()\"\n [tabIndex]=\"-1\"\n [id]=\"listId()\"\n role=\"listbox\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-multiselectable]=\"listService.selectableOptions().mode === 'multiple' || null\">\n @for (item of listService.viewItems(); track item.uid) {\n <ng-container\n [ngTemplateOutlet]=\"listItemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"></ng-container>\n }\n </ul>\n } @else if (noDataTemplate()) {\n <ng-container [ngTemplateOutlet]=\"noDataTemplate()\"></ng-container>\n } @else {\n <mona-placeholder [text]=\"messages().noData\" class=\"w-full h-full flex items-center justify-center\"></mona-placeholder>\n }\n}\n@if (footerTemplate()) {\n <ng-container [ngTemplateOutlet]=\"footerTemplate()\"></ng-container>\n}\n\n<ng-template #listItemTemplate let-item>\n @if (item.header) {\n <li [class]=\"groupHeaderClasses()\" [style]=\"listItemStyle()\">\n <mona-list-item [item]=\"item\" [template]=\"groupHeaderTemplate()\"></mona-list-item>\n </li>\n } @else {\n <li\n [class]=\"listItemClass()\"\n [class.px-0.5]=\"true\"\n [style]=\"listItemStyle()\"\n [id]=\"item.uid\"\n [attr.data-uid]=\"item.uid\"\n (click)=\"onListItemClick(item)\"\n monaListItem\n [item]=\"item\">\n <mona-list-item\n [item]=\"item\"\n [template]=\"itemTemplate()\"\n (checked)=\"onListItemCheck(item)\"></mona-list-item>\n </li>\n }\n</ng-template>\n", dependencies: [{ kind: "component", type: FilterInputComponent, selector: "mona-filter-input", inputs: ["debounce", "filter", "placeholder", "size"], outputs: ["filterChange", "inputBlur", "inputFocus"] }, { kind: "component", type: ListItemComponent, selector: "mona-list-item", inputs: ["item", "template"], outputs: ["checked"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: ListItemDirective, selector: "li[monaListItem]", inputs: ["item"] }, { kind: "component", type: PlaceholderComponent, selector: "mona-placeholder", inputs: ["text", "class"] }, { kind: "component", type: CdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "directive", type: CdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: CdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }] }); }
1520
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: ListComponent, isStandalone: true, selector: "mona-list", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, focusOnMount: { classPropertyName: "focusOnMount", publicName: "focusOnMount", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, listClass: { classPropertyName: "listClass", publicName: "listClass", isSignal: true, isRequired: false, transformFunction: null }, listId: { classPropertyName: "listId", publicName: "listId", isSignal: true, isRequired: false, transformFunction: null }, listItemClass: { classPropertyName: "listItemClass", publicName: "listItemClass", isSignal: true, isRequired: false, transformFunction: null }, listItemStyle: { classPropertyName: "listItemStyle", publicName: "listItemStyle", isSignal: true, isRequired: false, transformFunction: null }, listStyle: { classPropertyName: "listStyle", publicName: "listStyle", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, maxWidth: { classPropertyName: "maxWidth", publicName: "maxWidth", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, textField: { classPropertyName: "textField", publicName: "textField", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemSelect: "itemSelect" }, host: { properties: { "attr.tabindex": "hostTabIndex()", "class": "classes()", "style.height": "listHeight()", "style.max-height": "listMaxHeight()", "style.max-width": "listMaxWidth()", "style.width": "listWidth()" } }, queries: [{ propertyName: "footerTemplate", first: true, predicate: ListFooterTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "groupHeaderTemplate", first: true, predicate: ListGroupHeaderTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "headerTemplate", first: true, predicate: ListHeaderTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "itemTemplate", first: true, predicate: ListItemTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "noDataTemplate", first: true, predicate: ListNoDataTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }], viewQueries: [{ propertyName: "filterInput", first: true, predicate: FilterInputComponent, descendants: true, isSignal: true }, { propertyName: "virtualScrollViewport", first: true, predicate: CdkVirtualScrollViewport, descendants: true, isSignal: true }], ngImport: i0, template: "@if (headerTemplate()) {\n <ng-container [ngTemplateOutlet]=\"headerTemplate()\"></ng-container>\n}\n@if (listService.filterableOptions().enabled && listService.filterInputVisible()) {\n <div class=\"p-2\">\n <mona-filter-input\n [filter]=\"listService.filterText()\"\n [debounce]=\"listService.filterableOptions().debounce\"\n [placeholder]=\"listService.filterPlaceholder()\"\n [ariaLabel]=\"messages().filterList\"\n [controls]=\"effectiveListId()\"\n [activeDescendant]=\"activeDescendant()\"\n [size]=\"size()\"\n (filterChange)=\"onFilterChange($event)\"></mona-filter-input>\n </div>\n}\n@if (listService.virtualScrollOptions().enabled) {\n @if (listService.viewItems().length !== 0) {\n <cdk-virtual-scroll-viewport\n [itemSize]=\"listService.virtualScrollOptions().height\"\n [style.height]=\"viewportHeight()\">\n <ul\n [class]=\"innerListClasses()\"\n [style]=\"listStyle()\"\n [tabIndex]=\"-1\"\n [id]=\"effectiveListId()\"\n role=\"listbox\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-multiselectable]=\"listService.selectableOptions().mode === 'multiple' || null\">\n <ng-container *cdkVirtualFor=\"let item of listService.viewItems(); trackBy: trackByUid\">\n <ng-container\n [ngTemplateOutlet]=\"listItemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"></ng-container>\n </ng-container>\n </ul>\n </cdk-virtual-scroll-viewport>\n } @else if (noDataTemplate()) {\n <ng-container [ngTemplateOutlet]=\"noDataTemplate()\"></ng-container>\n } @else {\n <mona-placeholder\n [text]=\"messages().noData\"\n class=\"w-full h-full flex items-center justify-center\"></mona-placeholder>\n }\n} @else {\n @if (listService.viewItems().length !== 0) {\n <ul\n [class]=\"innerListClasses()\"\n [style]=\"listStyle()\"\n [tabIndex]=\"-1\"\n [id]=\"effectiveListId()\"\n role=\"listbox\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-multiselectable]=\"listService.selectableOptions().mode === 'multiple' || null\">\n @for (item of listService.viewItems(); track item.uid) {\n <ng-container\n [ngTemplateOutlet]=\"listItemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"></ng-container>\n }\n </ul>\n } @else if (noDataTemplate()) {\n <ng-container [ngTemplateOutlet]=\"noDataTemplate()\"></ng-container>\n } @else {\n <mona-placeholder\n [text]=\"messages().noData\"\n class=\"w-full h-full flex items-center justify-center\"></mona-placeholder>\n }\n}\n@if (footerTemplate()) {\n <ng-container [ngTemplateOutlet]=\"footerTemplate()\"></ng-container>\n}\n\n<ng-template #listItemTemplate let-item>\n @if (item.isHeader) {\n <li [class]=\"groupHeaderClasses()\" [style]=\"listItemStyle()\" [id]=\"item.uid\" role=\"presentation\">\n <mona-list-item [item]=\"item\" [template]=\"groupHeaderTemplate()\"></mona-list-item>\n </li>\n } @else {\n <li\n [class]=\"listItemClass()\"\n [class.px-0.5]=\"true\"\n [style]=\"listItemStyle()\"\n [id]=\"item.uid\"\n [attr.data-uid]=\"item.uid\"\n (click)=\"onListItemClick(item)\"\n monaListItem\n [item]=\"item\">\n <mona-list-item [item]=\"item\" [template]=\"itemTemplate()\"></mona-list-item>\n </li>\n }\n</ng-template>\n", dependencies: [{ kind: "component", type: FilterInputComponent, selector: "mona-filter-input", inputs: ["activeDescendant", "ariaLabel", "controls", "debounce", "filter", "placeholder", "size"], outputs: ["filterChange", "inputBlur", "inputFocus"] }, { kind: "component", type: ListItemComponent, selector: "mona-list-item", inputs: ["item", "template"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: ListItemDirective, selector: "li[monaListItem]", inputs: ["item"] }, { kind: "component", type: PlaceholderComponent, selector: "mona-placeholder", inputs: ["text", "class"] }, { kind: "component", type: CdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "directive", type: CdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: CdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }] }); }
1315
1521
  }
1316
1522
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ListComponent, decorators: [{
1317
1523
  type: Component,
@@ -1331,7 +1537,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
1331
1537
  "[style.max-height]": "listMaxHeight()",
1332
1538
  "[style.max-width]": "listMaxWidth()",
1333
1539
  "[style.width]": "listWidth()"
1334
- }, template: "@if (headerTemplate()) {\n <ng-container [ngTemplateOutlet]=\"headerTemplate()\"></ng-container>\n}\n@if (listService.filterableOptions().enabled && listService.filterInputVisible()) {\n <div class=\"p-2\">\n <mona-filter-input\n [filter]=\"listService.filterText()\"\n [debounce]=\"listService.filterableOptions().debounce\"\n [placeholder]=\"listService.filterPlaceholder()\"\n [size]=\"size()\"\n (filterChange)=\"onFilterChange($event)\"></mona-filter-input>\n </div>\n}\n@if (listService.virtualScrollOptions().enabled) {\n @if (listService.viewItems().length !== 0) {\n <cdk-virtual-scroll-viewport\n [itemSize]=\"listService.virtualScrollOptions().height\"\n [style.height]=\"viewportHeight()\">\n <ul\n [class]=\"innerListClasses()\"\n [style]=\"listStyle()\"\n [tabIndex]=\"-1\"\n [id]=\"listId()\"\n role=\"listbox\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-multiselectable]=\"listService.selectableOptions().mode === 'multiple' || null\">\n <ng-container *cdkVirtualFor=\"let item of listService.viewItems()\">\n <ng-container\n [ngTemplateOutlet]=\"listItemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"></ng-container>\n </ng-container>\n </ul>\n </cdk-virtual-scroll-viewport>\n } @else if (noDataTemplate()) {\n <ng-container [ngTemplateOutlet]=\"noDataTemplate()\"></ng-container>\n } @else {\n <mona-placeholder [text]=\"messages().noData\" class=\"w-full h-full flex items-center justify-center\"></mona-placeholder>\n }\n} @else {\n @if (listService.viewItems().length !== 0) {\n <ul\n [class]=\"innerListClasses()\"\n [style]=\"listStyle()\"\n [tabIndex]=\"-1\"\n [id]=\"listId()\"\n role=\"listbox\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-multiselectable]=\"listService.selectableOptions().mode === 'multiple' || null\">\n @for (item of listService.viewItems(); track item.uid) {\n <ng-container\n [ngTemplateOutlet]=\"listItemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"></ng-container>\n }\n </ul>\n } @else if (noDataTemplate()) {\n <ng-container [ngTemplateOutlet]=\"noDataTemplate()\"></ng-container>\n } @else {\n <mona-placeholder [text]=\"messages().noData\" class=\"w-full h-full flex items-center justify-center\"></mona-placeholder>\n }\n}\n@if (footerTemplate()) {\n <ng-container [ngTemplateOutlet]=\"footerTemplate()\"></ng-container>\n}\n\n<ng-template #listItemTemplate let-item>\n @if (item.header) {\n <li [class]=\"groupHeaderClasses()\" [style]=\"listItemStyle()\">\n <mona-list-item [item]=\"item\" [template]=\"groupHeaderTemplate()\"></mona-list-item>\n </li>\n } @else {\n <li\n [class]=\"listItemClass()\"\n [class.px-0.5]=\"true\"\n [style]=\"listItemStyle()\"\n [id]=\"item.uid\"\n [attr.data-uid]=\"item.uid\"\n (click)=\"onListItemClick(item)\"\n monaListItem\n [item]=\"item\">\n <mona-list-item\n [item]=\"item\"\n [template]=\"itemTemplate()\"\n (checked)=\"onListItemCheck(item)\"></mona-list-item>\n </li>\n }\n</ng-template>\n" }]
1540
+ }, template: "@if (headerTemplate()) {\n <ng-container [ngTemplateOutlet]=\"headerTemplate()\"></ng-container>\n}\n@if (listService.filterableOptions().enabled && listService.filterInputVisible()) {\n <div class=\"p-2\">\n <mona-filter-input\n [filter]=\"listService.filterText()\"\n [debounce]=\"listService.filterableOptions().debounce\"\n [placeholder]=\"listService.filterPlaceholder()\"\n [ariaLabel]=\"messages().filterList\"\n [controls]=\"effectiveListId()\"\n [activeDescendant]=\"activeDescendant()\"\n [size]=\"size()\"\n (filterChange)=\"onFilterChange($event)\"></mona-filter-input>\n </div>\n}\n@if (listService.virtualScrollOptions().enabled) {\n @if (listService.viewItems().length !== 0) {\n <cdk-virtual-scroll-viewport\n [itemSize]=\"listService.virtualScrollOptions().height\"\n [style.height]=\"viewportHeight()\">\n <ul\n [class]=\"innerListClasses()\"\n [style]=\"listStyle()\"\n [tabIndex]=\"-1\"\n [id]=\"effectiveListId()\"\n role=\"listbox\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-multiselectable]=\"listService.selectableOptions().mode === 'multiple' || null\">\n <ng-container *cdkVirtualFor=\"let item of listService.viewItems(); trackBy: trackByUid\">\n <ng-container\n [ngTemplateOutlet]=\"listItemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"></ng-container>\n </ng-container>\n </ul>\n </cdk-virtual-scroll-viewport>\n } @else if (noDataTemplate()) {\n <ng-container [ngTemplateOutlet]=\"noDataTemplate()\"></ng-container>\n } @else {\n <mona-placeholder\n [text]=\"messages().noData\"\n class=\"w-full h-full flex items-center justify-center\"></mona-placeholder>\n }\n} @else {\n @if (listService.viewItems().length !== 0) {\n <ul\n [class]=\"innerListClasses()\"\n [style]=\"listStyle()\"\n [tabIndex]=\"-1\"\n [id]=\"effectiveListId()\"\n role=\"listbox\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-multiselectable]=\"listService.selectableOptions().mode === 'multiple' || null\">\n @for (item of listService.viewItems(); track item.uid) {\n <ng-container\n [ngTemplateOutlet]=\"listItemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"></ng-container>\n }\n </ul>\n } @else if (noDataTemplate()) {\n <ng-container [ngTemplateOutlet]=\"noDataTemplate()\"></ng-container>\n } @else {\n <mona-placeholder\n [text]=\"messages().noData\"\n class=\"w-full h-full flex items-center justify-center\"></mona-placeholder>\n }\n}\n@if (footerTemplate()) {\n <ng-container [ngTemplateOutlet]=\"footerTemplate()\"></ng-container>\n}\n\n<ng-template #listItemTemplate let-item>\n @if (item.isHeader) {\n <li [class]=\"groupHeaderClasses()\" [style]=\"listItemStyle()\" [id]=\"item.uid\" role=\"presentation\">\n <mona-list-item [item]=\"item\" [template]=\"groupHeaderTemplate()\"></mona-list-item>\n </li>\n } @else {\n <li\n [class]=\"listItemClass()\"\n [class.px-0.5]=\"true\"\n [style]=\"listItemStyle()\"\n [id]=\"item.uid\"\n [attr.data-uid]=\"item.uid\"\n (click)=\"onListItemClick(item)\"\n monaListItem\n [item]=\"item\">\n <mona-list-item [item]=\"item\" [template]=\"itemTemplate()\"></mona-list-item>\n </li>\n }\n</ng-template>\n" }]
1335
1541
  }], ctorParameters: () => [], propDecorators: { filterInput: [{ type: i0.ViewChild, args: [i0.forwardRef(() => FilterInputComponent), { isSignal: true }] }], footerTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ListFooterTemplateDirective), { ...{ read: TemplateRef }, isSignal: true }] }], groupHeaderTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ListGroupHeaderTemplateDirective), { ...{ read: TemplateRef }, isSignal: true }] }], headerTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ListHeaderTemplateDirective), { ...{ read: TemplateRef }, isSignal: true }] }], itemTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ListItemTemplateDirective), { ...{ read: TemplateRef }, isSignal: true }] }], noDataTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ListNoDataTemplateDirective), { ...{ read: TemplateRef }, isSignal: true }] }], virtualScrollViewport: [{ type: i0.ViewChild, args: [i0.forwardRef(() => CdkVirtualScrollViewport), { isSignal: true }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], focusOnMount: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusOnMount", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], itemSelect: [{ type: i0.Output, args: ["itemSelect"] }], listClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "listClass", required: false }] }], listId: [{ type: i0.Input, args: [{ isSignal: true, alias: "listId", required: false }] }], listItemClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "listItemClass", required: false }] }], listItemStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "listItemStyle", required: false }] }], listStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "listStyle", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], maxWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxWidth", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], textField: [{ type: i0.Input, args: [{ isSignal: true, alias: "textField", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }] } });
1336
1542
 
1337
1543
  /*