@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,9 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { inject, ElementRef, Renderer2, input, effect, Directive } from '@angular/core';
3
- import { forEach, compact } from '@mirei/ts-collections';
3
+ import { forEach } from '@mirei/ts-collections';
4
4
  import { Observable, timer, take, debounceTime, map, merge, scan, filter } from 'rxjs';
5
5
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
- import { isIterable } from 'rxjs/internal/util/isIterable';
7
6
 
8
7
  class AttributeBinderDirective {
9
8
  #element;
@@ -111,23 +110,16 @@ const hsva2rgba = (hue, saturation, value, alpha) => {
111
110
  };
112
111
  };
113
112
 
113
+ const toHexByte = (value) => {
114
+ const byte = Math.min(255, Math.max(0, Math.round(value)));
115
+ return byte.toString(16).padStart(2, "0");
116
+ };
114
117
  const rgba2hex = (r, g, b, a) => {
115
118
  if (r == null || g == null || b == null || a == null) {
116
119
  return "";
117
120
  }
118
- let rHex = r.toString(16);
119
- let gHex = g.toString(16);
120
- let bHex = b.toString(16);
121
- let aHex = a.toString(16);
122
- if (rHex.length == 1)
123
- rHex = "0" + rHex;
124
- if (gHex.length == 1)
125
- gHex = "0" + gHex;
126
- if (bHex.length == 1)
127
- bHex = "0" + bHex;
128
- if (aHex.length == 1)
129
- aHex = "0" + aHex;
130
- return `#${rHex}${gHex}${bHex}${aHex.toUpperCase() !== "FF" ? aHex : ""}`;
121
+ const aHex = toHexByte(a);
122
+ return `#${toHexByte(r)}${toHexByte(g)}${toHexByte(b)}${aHex !== "ff" ? aHex : ""}`;
131
123
  };
132
124
 
133
125
  const rgba2hsla = (red, green, blue, alpha) => {
@@ -179,10 +171,9 @@ const rgba2hsva = (r, g, b, a) => {
179
171
  const max = Math.max(r, g, b);
180
172
  const min = Math.min(r, g, b);
181
173
  let h = 0;
182
- let s;
183
174
  const v = max;
184
175
  const d = max - min;
185
- s = max === 0 ? 0 : d / max;
176
+ const s = max === 0 ? 0 : d / max;
186
177
  if (max !== min) {
187
178
  if (max === r) {
188
179
  h = (g - b) / d + (g < b ? 6 : 0);
@@ -231,7 +222,8 @@ const string2rgba = (rgbText) => {
231
222
 
232
223
  /**
233
224
  * Generates a unique control ID for an element.
234
- * The ID consists of a prefix, a random hexadecimal string, and a suffix.
225
+ * The ID consists of the `mona-` prefix followed by a random hexadecimal string.
226
+ * It contains only characters that are safe in CSS selectors, so it can be used with `querySelector("#…")` unescaped.
235
227
  * @param {number} [level=0] - The nesting level of the element. This increases the length of the generated ID.
236
228
  * @param {number} [length=7] - The base length of the random part of the ID.
237
229
  * @returns {string} A unique control ID string for use for elements.
@@ -245,14 +237,14 @@ const createElementControlId = (level = 0, length = 7) => {
245
237
  const randomId = Array.from(array, byte => byte.toString(16).padStart(2, "0"))
246
238
  .join("")
247
239
  .substring(0, idLength);
248
- return `${prefix}:${randomId}:`;
240
+ return `${prefix}${randomId}`;
249
241
  }
250
242
  let hex = "";
251
243
  const chars = "0123456789abcdef";
252
244
  for (let i = 0; i < idLength; i++) {
253
245
  hex += chars[Math.floor(Math.random() * 16)];
254
246
  }
255
- return `${prefix}:${hex}:`;
247
+ return `${prefix}${hex}`;
256
248
  };
257
249
 
258
250
  function focusElement(host, element) {
@@ -272,7 +264,7 @@ function focusElement(host, element) {
272
264
 
273
265
  const rxFromResize = (target, options) => {
274
266
  return new Observable(observer => {
275
- const elements = Array.isArray(target) ? target : [target];
267
+ const elements = target instanceof Element ? [target] : Array.from(target ?? []);
276
268
  for (const element of elements) {
277
269
  if (!(element instanceof Element)) {
278
270
  observer.error(new TypeError("target must be an Element or an iterable of Elements"));
@@ -348,18 +340,22 @@ const findScrollableElement = (container) => {
348
340
  return null;
349
341
  };
350
342
 
343
+ const toPixels = (value) => (value != null ? `${value}px` : "");
351
344
  const setWindowStyles = (element, settings) => {
352
345
  if (!element) {
353
346
  return;
354
347
  }
355
- const classes = isIterable(settings.windowClass) ? [...settings.windowClass] : [settings.windowClass];
356
- compact(classes).forEach(className => element.classList.add(className));
357
- const minWidth = settings.minWidth ? `${settings.minWidth}px` : "";
358
- const minHeight = settings.minHeight ? `${settings.minHeight}px` : "";
359
- const maxWidth = settings.maxWidth ? `${settings.maxWidth}px` : "";
360
- const maxHeight = settings.maxHeight ? `${settings.maxHeight}px` : "";
361
- const top = settings.top ? `${settings.top}px` : `calc(50% - ${element.getBoundingClientRect().height / 2}px)`;
362
- const left = settings.left ? `${settings.left}px` : `calc(50% - ${element.getBoundingClientRect().width / 2}px)`;
348
+ const classes = typeof settings.windowClass === "string" ? [settings.windowClass] : (settings.windowClass ?? []);
349
+ classes
350
+ .flatMap(className => (className ?? "").split(/\s+/))
351
+ .filter(className => className !== "")
352
+ .forEach(className => element.classList.add(className));
353
+ const minWidth = toPixels(settings.minWidth);
354
+ const minHeight = toPixels(settings.minHeight);
355
+ const maxWidth = toPixels(settings.maxWidth);
356
+ const maxHeight = toPixels(settings.maxHeight);
357
+ const top = settings.top != null ? `${settings.top}px` : `calc(50% - ${element.getBoundingClientRect().height / 2}px)`;
358
+ const left = settings.left != null ? `${settings.left}px` : `calc(50% - ${element.getBoundingClientRect().width / 2}px)`;
363
359
  const position = "absolute";
364
360
  Object.assign(element.style, { left, maxHeight, maxWidth, minHeight, minWidth, position, top });
365
361
  };
@@ -374,7 +370,7 @@ const setWindowStyles = (element, settings) => {
374
370
  * @returns A CSS-compliant string value.
375
371
  */
376
372
  const toCssValue = (value) => {
377
- if (value == null || value === "") {
373
+ if (value == null || (typeof value === "string" && value.trim() === "")) {
378
374
  return "";
379
375
  }
380
376
  if (typeof value === "number" || isFinite(Number(value))) {
@@ -402,8 +398,15 @@ const isNavigationKey = (key) => {
402
398
  return NavigationKeysList.includes(key);
403
399
  };
404
400
 
405
- const isTypeaheadKey = (key) => {
406
- return /^[a-zA-Z0-9\-_\s]$/.test(key);
401
+ /**
402
+ * Returns whether a `KeyboardEvent.key` value should feed typeahead.
403
+ * Any single letter or digit in any script counts, plus space, `-` and `_`.
404
+ */
405
+ const isTypeaheadKey = (key, modifiers) => {
406
+ if (modifiers?.ctrlKey || modifiers?.metaKey || modifiers?.altKey) {
407
+ return false;
408
+ }
409
+ return /^[\p{L}\p{M}\p{N}\-_\s]$/u.test(key);
407
410
  };
408
411
  const setupTypeahead = (typeaheadKey$) => {
409
412
  const resetTrigger$ = typeaheadKey$.pipe(debounceTime(200), map(() => ({ type: "reset", value: "" })));
@@ -260,6 +260,7 @@ const AR_SA_MESSAGES = {
260
260
  selectRow: (rowNumber) => `تحديد الصف ${formatInteger(rowNumber)}`
261
261
  },
262
262
  list: {
263
+ filterList: "تصفية القائمة",
263
264
  noData: "لا توجد بيانات"
264
265
  },
265
266
  listBox: {
@@ -639,6 +640,7 @@ const ES_ES_MESSAGES = {
639
640
  selectRow: (rowNumber) => `Seleccionar fila ${rowNumber}`
640
641
  },
641
642
  list: {
643
+ filterList: "Filtrar lista",
642
644
  noData: "No hay datos"
643
645
  },
644
646
  listBox: {
@@ -1018,6 +1020,7 @@ const DE_DE_MESSAGES = {
1018
1020
  selectRow: (rowNumber) => `Zeile ${rowNumber} auswählen`
1019
1021
  },
1020
1022
  list: {
1023
+ filterList: "Liste filtern",
1021
1024
  noData: "Keine Daten"
1022
1025
  },
1023
1026
  listBox: {
@@ -1397,6 +1400,7 @@ const FR_FR_MESSAGES = {
1397
1400
  selectRow: (rowNumber) => `Sélectionner la ligne ${rowNumber}`
1398
1401
  },
1399
1402
  list: {
1403
+ filterList: "Filtrer la liste",
1400
1404
  noData: "Aucune donnée"
1401
1405
  },
1402
1406
  listBox: {
@@ -1776,6 +1780,7 @@ const IT_IT_MESSAGES = {
1776
1780
  selectRow: (rowNumber) => `Seleziona riga ${rowNumber}`
1777
1781
  },
1778
1782
  list: {
1783
+ filterList: "Filtra elenco",
1779
1784
  noData: "Nessun dato disponibile"
1780
1785
  },
1781
1786
  listBox: {
@@ -2155,6 +2160,7 @@ const JA_JP_MESSAGES = {
2155
2160
  selectRow: (rowNumber) => `${rowNumber}行目を選択`
2156
2161
  },
2157
2162
  list: {
2163
+ filterList: "リストをフィルター",
2158
2164
  noData: "データがありません"
2159
2165
  },
2160
2166
  listBox: {
@@ -2534,6 +2540,7 @@ const KO_KR_MESSAGES = {
2534
2540
  selectRow: (rowNumber) => `${rowNumber}행 선택`
2535
2541
  },
2536
2542
  list: {
2543
+ filterList: "목록 필터",
2537
2544
  noData: "데이터 없음"
2538
2545
  },
2539
2546
  listBox: {
@@ -2913,6 +2920,7 @@ const PT_BR_MESSAGES = {
2913
2920
  selectRow: (rowNumber) => `Selecionar linha ${rowNumber}`
2914
2921
  },
2915
2922
  list: {
2923
+ filterList: "Filtrar lista",
2916
2924
  noData: "Nenhum dado disponível"
2917
2925
  },
2918
2926
  listBox: {
@@ -3292,6 +3300,7 @@ const ZH_CN_MESSAGES = {
3292
3300
  selectRow: (rowNumber) => `选择第 ${rowNumber} 行`
3293
3301
  },
3294
3302
  list: {
3303
+ filterList: "筛选列表",
3295
3304
  noData: "没有数据"
3296
3305
  },
3297
3306
  listBox: {
@@ -3671,6 +3680,7 @@ const ZH_TW_MESSAGES = {
3671
3680
  selectRow: (rowNumber) => `選取第 ${rowNumber} 個資料列`
3672
3681
  },
3673
3682
  list: {
3683
+ filterList: "篩選清單",
3674
3684
  noData: "沒有資料"
3675
3685
  },
3676
3686
  listBox: {
@@ -4050,6 +4060,7 @@ const TR_TR_MESSAGES = {
4050
4060
  selectRow: (rowNumber) => `${rowNumber}. satırı seç`
4051
4061
  },
4052
4062
  list: {
4063
+ filterList: "Listeyi filtrele",
4053
4064
  noData: "Veri yok"
4054
4065
  },
4055
4066
  listBox: {
@@ -4429,6 +4440,7 @@ const ID_ID_MESSAGES = {
4429
4440
  selectRow: (rowNumber) => `Pilih baris ${rowNumber}`
4430
4441
  },
4431
4442
  list: {
4443
+ filterList: "Saring daftar",
4432
4444
  noData: "Tidak ada data"
4433
4445
  },
4434
4446
  listBox: {
@@ -4837,6 +4849,7 @@ const RU_RU_MESSAGES = {
4837
4849
  selectRow: (rowNumber) => `Выбрать строку ${rowNumber}`
4838
4850
  },
4839
4851
  list: {
4852
+ filterList: "Фильтр списка",
4840
4853
  noData: "Нет данных"
4841
4854
  },
4842
4855
  listBox: {
@@ -545,7 +545,7 @@ class PopupMenuListComponent {
545
545
  this.#typeaheadKey$.next("");
546
546
  this.handleNavigationKey(e);
547
547
  }
548
- else if (isTypeaheadKey(e.key)) {
548
+ else if (isTypeaheadKey(e.key, e)) {
549
549
  e.preventDefault();
550
550
  this.handleTypeaheadKey(e);
551
551
  }
@@ -196,7 +196,7 @@ class TreeViewComponent {
196
196
  this.treeService.blur$.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe(e => this.blur.emit(e));
197
197
  }
198
198
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: TreeViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
199
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: TreeViewComponent, isStandalone: true, selector: "mona-tree-view", inputs: { animate: { classPropertyName: "animate", publicName: "animate", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, children: { classPropertyName: "children", publicName: "children", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, hasChildren: { classPropertyName: "hasChildren", publicName: "hasChildren", isSignal: true, isRequired: false, transformFunction: null }, idField: { classPropertyName: "idField", publicName: "idField", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, parentIdField: { classPropertyName: "parentIdField", publicName: "parentIdField", isSignal: true, isRequired: false, transformFunction: null }, textField: { classPropertyName: "textField", publicName: "textField", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blur: "blur", focus: "focus", nodeClick: "nodeClick" }, providers: [TreeService], queries: [{ propertyName: "nodeTemplate", first: true, predicate: TreeViewNodeTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }], ngImport: i0, template: "@if (treeService.filterableOptions().enabled) {\n <div role=\"search\" [attr.aria-label]=\"filterAriaLabel()\">\n <mona-filter-input\n [filter]=\"treeService.filterText()\"\n [debounce]=\"treeService.filterableOptions().debounce\"\n [placeholder]=\"treeService.filterPlaceholder()\"\n (filterChange)=\"onFilterChange($event)\"></mona-filter-input>\n </div>\n}\n<div class=\"flex-1 overflow-auto\">\n <mona-tree [ariaLabel]=\"ariaLabel()\">\n @if (nodeTemplate()) {\n <ng-template monaTreeNodeTemplate let-dataItem let-element=\"element\">\n <ng-container\n [ngTemplateOutlet]=\"nodeTemplate() ?? null\"\n [ngTemplateOutletContext]=\"{ $implicit: dataItem, element }\"></ng-container>\n </ng-template>\n }\n </mona-tree>\n</div>\n", dependencies: [{ kind: "component", type: FilterInputComponent, selector: "mona-filter-input", inputs: ["debounce", "filter", "placeholder", "size"], outputs: ["filterChange", "inputBlur", "inputFocus"] }, { kind: "component", type: TreeComponent, selector: "mona-tree", inputs: ["ariaLabel", "data"] }, { kind: "directive", type: TreeNodeTemplateDirective, selector: "ng-template[monaTreeNodeTemplate]" }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); }
199
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: TreeViewComponent, isStandalone: true, selector: "mona-tree-view", inputs: { animate: { classPropertyName: "animate", publicName: "animate", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, children: { classPropertyName: "children", publicName: "children", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, hasChildren: { classPropertyName: "hasChildren", publicName: "hasChildren", isSignal: true, isRequired: false, transformFunction: null }, idField: { classPropertyName: "idField", publicName: "idField", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, parentIdField: { classPropertyName: "parentIdField", publicName: "parentIdField", isSignal: true, isRequired: false, transformFunction: null }, textField: { classPropertyName: "textField", publicName: "textField", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blur: "blur", focus: "focus", nodeClick: "nodeClick" }, providers: [TreeService], queries: [{ propertyName: "nodeTemplate", first: true, predicate: TreeViewNodeTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }], ngImport: i0, template: "@if (treeService.filterableOptions().enabled) {\n <div role=\"search\" [attr.aria-label]=\"filterAriaLabel()\">\n <mona-filter-input\n [filter]=\"treeService.filterText()\"\n [debounce]=\"treeService.filterableOptions().debounce\"\n [placeholder]=\"treeService.filterPlaceholder()\"\n (filterChange)=\"onFilterChange($event)\"></mona-filter-input>\n </div>\n}\n<div class=\"flex-1 overflow-auto\">\n <mona-tree [ariaLabel]=\"ariaLabel()\">\n @if (nodeTemplate()) {\n <ng-template monaTreeNodeTemplate let-dataItem let-element=\"element\">\n <ng-container\n [ngTemplateOutlet]=\"nodeTemplate() ?? null\"\n [ngTemplateOutletContext]=\"{ $implicit: dataItem, element }\"></ng-container>\n </ng-template>\n }\n </mona-tree>\n</div>\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: TreeComponent, selector: "mona-tree", inputs: ["ariaLabel", "data"] }, { kind: "directive", type: TreeNodeTemplateDirective, selector: "ng-template[monaTreeNodeTemplate]" }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); }
200
200
  }
201
201
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: TreeViewComponent, decorators: [{
202
202
  type: Component,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanahoshi/mona-ui",
3
- "version": "0.23.0",
3
+ "version": "0.23.1",
4
4
  "description": "A very light Angular UI framework",
5
5
  "author": "Nanahoshi",
6
6
  "repository": {
@@ -271,6 +271,7 @@ interface MonaGridMessages {
271
271
  }
272
272
 
273
273
  interface MonaListMessages {
274
+ readonly filterList: string;
274
275
  readonly noData: string;
275
276
  }
276
277
 
@@ -7,6 +7,40 @@ declare class FilterInputComponent implements OnInit {
7
7
  #private;
8
8
  protected readonly filter$: Subject<string>;
9
9
  protected readonly filterText: _angular_core.WritableSignal<string>;
10
+ /**
11
+ * When the input controls a list, it acts as a combobox (WAI-ARIA APG): focus stays in the input
12
+ * and the highlighted option is exposed through `aria-activedescendant`.
13
+ */
14
+ protected readonly inputAttributes: _angular_core.Signal<{
15
+ "aria-label": string | null;
16
+ role?: undefined;
17
+ "aria-activedescendant"?: undefined;
18
+ "aria-autocomplete"?: undefined;
19
+ "aria-controls"?: undefined;
20
+ "aria-expanded"?: undefined;
21
+ } | {
22
+ role: string;
23
+ "aria-label": string | null;
24
+ "aria-activedescendant": string | null;
25
+ "aria-autocomplete": string;
26
+ "aria-controls": string;
27
+ "aria-expanded": boolean;
28
+ }>;
29
+ /**
30
+ * @description Id of the highlighted option in the controlled list, exposed as `aria-activedescendant`.
31
+ * @default ""
32
+ */
33
+ readonly activeDescendant: _angular_core.InputSignal<string | null>;
34
+ /**
35
+ * @description Accessible name of the input. The placeholder is not a reliable label, so set this whenever possible.
36
+ * @default ""
37
+ */
38
+ readonly ariaLabel: _angular_core.InputSignal<string>;
39
+ /**
40
+ * @description Id of the list this input filters. When set, the input is exposed as a combobox controlling it.
41
+ * @default ""
42
+ */
43
+ readonly controls: _angular_core.InputSignal<string>;
10
44
  readonly debounce: _angular_core.InputSignal<number>;
11
45
  readonly filter: _angular_core.InputSignal<string>;
12
46
  readonly filterChange: _angular_core.OutputEmitterRef<FilterChangeEvent>;
@@ -19,7 +53,7 @@ declare class FilterInputComponent implements OnInit {
19
53
  ngOnInit(): void;
20
54
  private setSubscriptions;
21
55
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<FilterInputComponent, never>;
22
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<FilterInputComponent, "mona-filter-input", never, { "debounce": { "alias": "debounce"; "required": false; "isSignal": true; }; "filter": { "alias": "filter"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; }, { "filterChange": "filterChange"; "inputBlur": "inputBlur"; "inputFocus": "inputFocus"; }, never, never, true, never>;
56
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<FilterInputComponent, "mona-filter-input", never, { "activeDescendant": { "alias": "activeDescendant"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "controls": { "alias": "controls"; "required": false; "isSignal": true; }; "debounce": { "alias": "debounce"; "required": false; "isSignal": true; }; "filter": { "alias": "filter"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; }, { "filterChange": "filterChange"; "inputBlur": "inputBlur"; "inputFocus": "inputFocus"; }, never, never, true, never>;
23
57
  }
24
58
 
25
59
  export { FilterInputComponent };
@@ -10,10 +10,21 @@ import { Subject, ReplaySubject } from 'rxjs';
10
10
  interface ListItemOptions<T> {
11
11
  data: T;
12
12
  header?: string;
13
+ /**
14
+ * Marks the item as a group header. Defaults to `true` when a non-empty `header` is given.
15
+ * Set it explicitly for groups whose key is empty, which still need a header row.
16
+ */
17
+ isHeader?: boolean;
18
+ /**
19
+ * Reuses an existing identifier, so an item rebuilt for refreshed data keeps its DOM id and tracking.
20
+ * A new UUID is generated when omitted.
21
+ */
22
+ uid?: string;
13
23
  }
14
24
  declare class ListItem<TData> {
15
25
  readonly data: TData;
16
26
  readonly header: string;
27
+ readonly isHeader: boolean;
17
28
  readonly uid: string;
18
29
  constructor(options: ListItemOptions<TData>);
19
30
  }
@@ -108,11 +119,27 @@ declare class ListService<TData> {
108
119
  readonly focus$: Subject<void>;
109
120
  readonly focusableItem: _angular_core.Signal<ListItem<TData> | null>;
110
121
  readonly groupBy: _angular_core.WritableSignal<ListKeySelector<TData>>;
122
+ /**
123
+ * Maps each option uid to the uid of the group header it is listed under.
124
+ */
125
+ readonly groupHeaderUids: _angular_core.Signal<Map<string, string>>;
111
126
  readonly groupableOptions: _angular_core.WritableSignal<GroupableOptions<TData, any>>;
127
+ /**
128
+ * The highlighted (active) item. When the data changes, it is re-resolved to the item with the same `uid`,
129
+ * or cleared when that item is gone.
130
+ */
112
131
  readonly highlightedItem: _angular_core.WritableSignal<ListItem<TData> | null>;
113
132
  readonly itemFocusOut$: Subject<ListItem<TData>>;
114
- readonly navigatedItem$: Subject<ListItem<TData> | null>;
133
+ /**
134
+ * Position of each option among all filtered options (not only the current page), for `aria-posinset`
135
+ * and `aria-setsize`.
136
+ */
137
+ readonly itemPositions: _angular_core.Signal<Map<string, {
138
+ position: number;
139
+ total: number;
140
+ }>>;
115
141
  readonly navigableOptions: _angular_core.WritableSignal<NavigableOptions>;
142
+ readonly navigatedItem$: Subject<ListItem<TData> | null>;
116
143
  readonly pageState: _angular_core.WritableSignal<PageState>;
117
144
  readonly pageableOptions: _angular_core.WritableSignal<PagerSettings>;
118
145
  readonly scrollToItem$: ReplaySubject<{
@@ -126,16 +153,20 @@ declare class ListService<TData> {
126
153
  readonly selectionChange$: Subject<ListItem<TData>>;
127
154
  readonly textField: _angular_core.WritableSignal<string | Selector<TData, string>>;
128
155
  readonly valueField: _angular_core.WritableSignal<string | Selector<TData, unknown>>;
156
+ /**
157
+ * Visible items in display order: filtered, ordered and grouped (with header rows), but not paged.
158
+ */
129
159
  readonly viewItems: _angular_core.Signal<ImmutableSet<ListItem<TData>>>;
130
160
  readonly virtualScrollOptions: _angular_core.WritableSignal<VirtualScrollOptions>;
131
- readonly itemPositions: _angular_core.Signal<Map<string, {
132
- position: number;
133
- total: number;
134
- }>>;
135
- getItemPosition(item: ListItem<TData>): {
136
- position: number;
137
- total: number;
138
- } | null;
161
+ clearFilter(): void;
162
+ clearHighlightedItem(): void;
163
+ clearSelections(): void;
164
+ /**
165
+ * Finds the next item whose text starts with the typeahead buffer, starting after the active item.
166
+ * Group headers and disabled items are never matched.
167
+ */
168
+ cycleThroughMatchedItems(buffer: string): ListItem<TData> | null;
169
+ deselectItems(item: Iterable<ListItem<TData>>): void;
139
170
  /**
140
171
  * Resolves the selection value of a data item through `valueField`.
141
172
  *
@@ -146,14 +177,25 @@ declare class ListService<TData> {
146
177
  * Falsy results such as `0`, `false`, and `""` are valid values and do not fall back to the item.
147
178
  */
148
179
  getDataItemValue(dataItem: TData): unknown;
149
- clearFilter(): void;
150
- clearHighlightedItem(): void;
151
- clearSelections(): void;
152
- cycleThroughMatchedItems(buffer: string): ListItem<TData> | null;
153
- deselectItems(item: Iterable<ListItem<TData>>): void;
154
180
  getFilterDebounceDuration(): number;
155
181
  getGroupField(item: ListItem<TData>): any;
182
+ /**
183
+ * Returns the uid of the group header an item is rendered under, or `null` when the list is not grouped.
184
+ */
185
+ getGroupHeaderUid(item: ListItem<TData>): string | null;
186
+ getItemPosition(item: ListItem<TData>): {
187
+ position: number;
188
+ total: number;
189
+ } | null;
190
+ /**
191
+ * Resolves the display text of an item through `textField`.
192
+ * Non-string field values (numbers, dates, …) are converted with `String()`, and nullish values become `""`.
193
+ */
156
194
  getItemText(item: ListItem<TData>): string;
195
+ /**
196
+ * Finds the item that typed text should resolve to: an exact text match first (respecting `caseSensitive`),
197
+ * otherwise the first item accepted by the filter operator. Group headers and disabled items are skipped.
198
+ */
157
199
  getMatchingFilteredItem(text: string): ListItem<TData> | null;
158
200
  isDisabled(item: ListItem<TData>): boolean;
159
201
  isFilteringEnabled(): boolean;
@@ -162,6 +204,11 @@ declare class ListService<TData> {
162
204
  navigate(direction: NavigationDirection, mode: NavigationMode, focus: boolean): ListItem<TData> | null;
163
205
  notifySelectionChange(item: ListItem<TData>): void;
164
206
  scrollToItem(item: ListItem<TData>, focus: boolean): void;
207
+ /**
208
+ * Selects every enabled item currently in view, keeping existing selections. Only applies in multiple mode.
209
+ * @returns whether the selection changed.
210
+ */
211
+ selectAllItems(): boolean;
165
212
  selectItem(item: ListItem<TData>, scroll?: boolean, focus?: boolean): void;
166
213
  setData(data: Iterable<TData>): void;
167
214
  setDisabledBy(disabledBy: string | Predicate<TData>): void;
@@ -179,11 +226,16 @@ declare class ListService<TData> {
179
226
  setValueField(valueField: string | Selector<TData, unknown>): void;
180
227
  setVirtualScrollOptions(options: VirtualScrollOptions): void;
181
228
  private filterItem;
229
+ /**
230
+ * Returns the item when it is one of the navigable items, otherwise `null`.
231
+ * An anchor that was filtered out or removed must not be navigated from.
232
+ */
233
+ private getItemInView;
182
234
  private getOrderKey;
183
235
  private getPreviousItemForNavigation;
184
236
  private getSelectionKey;
185
- private navigateForMultipleSelection;
186
237
  private navigateFirstForSingleSelection;
238
+ private navigateForMultipleSelection;
187
239
  private navigateForSingleSelection;
188
240
  private navigateLastForSingleSelection;
189
241
  private navigateNextForSingleSelection;
@@ -196,7 +248,15 @@ declare class ListService<TData> {
196
248
  declare class ListComponent<TData> implements OnInit {
197
249
  #private;
198
250
  private readonly filterInput;
251
+ /**
252
+ * Id of the highlighted option, used as `aria-activedescendant` by the filter input.
253
+ */
254
+ protected readonly activeDescendant: Signal<string | null>;
199
255
  protected readonly classes: Signal<string>;
256
+ /**
257
+ * The `listId` input, or a generated id so the listbox can always be referenced by `aria-controls`.
258
+ */
259
+ protected readonly effectiveListId: Signal<string>;
200
260
  protected readonly footerTemplate: Signal<TemplateRef<any> | undefined>;
201
261
  protected readonly groupHeaderClasses: Signal<string>;
202
262
  protected readonly groupHeaderTemplate: Signal<TemplateRef<any> | undefined>;
@@ -235,10 +295,16 @@ declare class ListComponent<TData> implements OnInit {
235
295
  constructor();
236
296
  ngOnInit(): void;
237
297
  protected onFilterChange(event: FilterChangeEvent): void;
238
- protected onListItemCheck(item: ListItem<TData>): void;
239
298
  protected onListItemClick(item: ListItem<TData>): void;
299
+ protected trackByUid(_: number, item: ListItem<TData>): string;
240
300
  private cycleThroughMatchedItems;
241
301
  private focusToItem;
302
+ /**
303
+ * Handles multiple-selection shortcuts from the WAI-ARIA listbox pattern:
304
+ * Space toggles the highlighted option and Ctrl/Cmd+A selects every enabled option.
305
+ * @returns whether the event was handled.
306
+ */
307
+ private handleMultipleSelectionKeys;
242
308
  private handleNavigation;
243
309
  private handlePageScroll;
244
310
  private scrollToItem;
@@ -67,6 +67,11 @@ declare class TreeNode<T> {
67
67
  readonly data: T;
68
68
  readonly loading: _angular_core.WritableSignal<boolean>;
69
69
  readonly uid: string;
70
+ /**
71
+ * Whether children from an asynchronous children selector have been loaded. Loaded children are cached,
72
+ * so collapsing and re-expanding the node does not request them again.
73
+ */
74
+ childrenLoaded: boolean;
70
75
  index: string;
71
76
  parent: TreeNode<T> | null;
72
77
  constructor(data: T);
@@ -269,6 +274,15 @@ declare class TreeService<T> {
269
274
  static flatten<T>(nodes: Iterable<TreeNode<T>>): List<TreeNode<T>>;
270
275
  clearFilter(): void;
271
276
  detachNode(node: TreeNode<T>): void;
277
+ /**
278
+ * Expands every expandable, enabled sibling of a node (the `*` key of the WAI-ARIA tree pattern).
279
+ */
280
+ expandSiblings(node: TreeNode<T>): void;
281
+ /**
282
+ * Finds the next visible node, after the navigated node and wrapping around, whose text starts with `prefix`
283
+ * (case-insensitive). Used for typeahead.
284
+ */
285
+ findNodeByText(prefix: string): TreeNode<T> | null;
272
286
  getNodeByUid(uid: string): TreeNode<T> | null;
273
287
  getNodeElementId(uid: string): string;
274
288
  getNodeText(node: TreeNode<T>): string;
@@ -283,7 +297,15 @@ declare class TreeService<T> {
283
297
  isSelected(node: TreeNode<T>): boolean;
284
298
  isTreeFiltered(): boolean;
285
299
  isVisible(node: TreeNode<T>): boolean;
286
- loadNodeChildren(node: TreeNode<T>): void;
300
+ /**
301
+ * Loads the children of a node from an asynchronous children selector.
302
+ * Children are loaded once and cached; pass `reload` to request them again. A new load cancels a pending one.
303
+ */
304
+ loadNodeChildren(node: TreeNode<T>, reload?: boolean): void;
305
+ /**
306
+ * Moves a node relative to a target node. Moving a node onto itself or into its own subtree is ignored,
307
+ * because it would detach the subtree and create a parent cycle.
308
+ */
287
309
  moveNode(sourceNode: TreeNode<T>, targetNode: TreeNode<T>, position: DropPosition): void;
288
310
  navigate(direction: "next" | "previous" | "first" | "last"): TreeNode<T> | null;
289
311
  notifySelectionChange(node: TreeNode<T>): void;
@@ -314,21 +336,40 @@ declare class TreeService<T> {
314
336
  setSelectedDataItems(dataItems: Iterable<T>): void;
315
337
  setSelectedKeys(keys: Iterable<any>): void;
316
338
  setTextField(selector: string | Selector<T, string>): void;
339
+ /**
340
+ * Expands or collapses a node the way user interaction does: disabled nodes are left alone, children of an
341
+ * asynchronous children selector are loaded on first expand, and `nodeExpand$` is notified.
342
+ * @returns whether the expand state changed.
343
+ */
344
+ toggleNodeExpand(node: TreeNode<T>, expanded: boolean): boolean;
317
345
  getCheckKey(node: TreeNode<T>): any;
318
346
  getDisableKey(node: TreeNode<T>): any;
319
347
  private applyCheckedKeysToNodes;
320
348
  private computeVisibleUids;
321
349
  private createFlatNodes;
350
+ /**
351
+ * Builds tree nodes for hierarchical data, reusing the existing node of each data item so node state survives
352
+ * a data refresh. A data object that appears more than once gets a node of its own for every extra occurrence.
353
+ */
322
354
  private createHierarchicalNodes;
323
355
  private createNodeDictionary;
324
356
  private createNodes;
357
+ /**
358
+ * The derived check state of a parent: `true` when all of its check-state children are checked,
359
+ * or `null` when it has none (its own state then stands).
360
+ */
361
+ private deriveParentChecked;
362
+ /**
363
+ * Children that determine a parent's derived check state: those with a checkbox and, unless
364
+ * `checkDisabledChildren` is set, only enabled ones, because the user cannot change disabled children.
365
+ */
366
+ private getCheckStateChildren;
325
367
  private getExpandKey;
326
368
  private getChildNodes;
327
369
  private getDataItemSelectKey;
328
370
  private getParentNodes;
329
371
  private getSelectKey;
330
372
  private isAnyParentCollapsed;
331
- private isAnyParentDisabled;
332
373
  private isFiltered;
333
374
  private loadCheckedKeysForChildren;
334
375
  private moveNodeAfter;
@@ -360,6 +401,7 @@ declare class TreeComponent<T> {
360
401
  private setKeydownSubscription;
361
402
  private setNodeDragHandlerSubscription;
362
403
  private setSubscriptions;
404
+ private setTypeaheadSubscription;
363
405
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TreeComponent<any>, never>;
364
406
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<TreeComponent<any>, "mona-tree", never, { "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "data": { "alias": "data"; "required": false; "isSignal": true; }; }, {}, ["nodeTemplate"], never, true, never>;
365
407
  }
@@ -378,6 +420,8 @@ declare const TREE_DEFAULT_MESSAGES: MonaTreeViewMessages;
378
420
  * @param idKey - The property key used to uniquely identify each node (e.g., 'id').
379
421
  * @param parentIdKey - The property key holding the parent node's id, or null for roots (e.g., 'parentId').
380
422
  * @returns A new array representing the updated flat tree, with the moved node's parentIdKey updated.
423
+ * The array is returned unchanged when the position is "outside" or the target is the dragged node or one of its
424
+ * descendants, which would otherwise create a parent cycle.
381
425
  */
382
426
  declare function moveFlatTreeNode<T extends Record<string, any>>(treeData: Iterable<T>, event: NodeDropEvent<T>, idKey: keyof T, parentIdKey: keyof T): T[];
383
427
 
@@ -387,7 +431,10 @@ declare function moveFlatTreeNode<T extends Record<string, any>>(treeData: Itera
387
431
  * @param event - The drag-and-drop event payload.
388
432
  * @param idKey - The property key used to identify nodes (e.g., 'id').
389
433
  * @param childrenKey - The property key containing the nested items (e.g., 'items').
390
- * @returns A new array representing the updated tree structure.
434
+ * @returns A new top-level array representing the updated tree structure.
435
+ * Nested children arrays are updated in place, so untouched data items keep their identity (and their tree state).
436
+ * The move is skipped, returning the tree unchanged, when the position is "outside", the target is missing,
437
+ * or the target is the dragged node or one of its descendants, so a node is never lost or made its own ancestor.
391
438
  */
392
439
  declare function moveTreeNode<T extends Record<string, any>>(treeData: Iterable<T>, event: NodeDropEvent<T>, idKey: keyof T, childrenKey: keyof T): T[];
393
440