ngwr 14.5.1 → 14.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,6 +39,21 @@ type WrSelectTagValidator = (value: string, existing: readonly string[]) => bool
39
39
  type WrSelectSearchLoader<T> = (query: string) => Observable<readonly T[]> | Promise<readonly T[]> | readonly T[];
40
40
  /** Control size for `<wr-select>` — shares the `--wr-control-*` contract. */
41
41
  type WrSelectSize = 'sm' | 'md' | 'lg';
42
+ /**
43
+ * Where a `wrOptionLeading` template is being drawn:
44
+ *
45
+ * - `'option'` — the row in the open panel.
46
+ * - `'chip'` — a selected chip on a `mode="multi"` trigger.
47
+ * - `'value'` — the selected value on a single-mode button trigger.
48
+ */
49
+ type WrOptionLeadingPlacement = 'option' | 'chip' | 'value';
50
+ /** Template context of `<ng-template wrOptionLeading>`. */
51
+ interface WrOptionLeadingContext<T = unknown> {
52
+ /** The option's value — `let-value`. */
53
+ readonly $implicit: T;
54
+ /** Where this copy is drawn, so one template can size itself per surface. */
55
+ readonly placement: WrOptionLeadingPlacement;
56
+ }
42
57
 
43
58
  /**
44
59
  * @license
@@ -67,6 +82,13 @@ interface WrSelectOptionRegistration {
67
82
  * registration that cannot supply an element simply keeps its place.
68
83
  */
69
84
  readonly host?: HTMLElement;
85
+ /**
86
+ * The option's own `wrOptionLeading` template, when it declares one. Chips
87
+ * look it up by value, since a chip is drawn by the select and not by the
88
+ * option. Optional, like `host`: a registration without one falls back to the
89
+ * select's default template.
90
+ */
91
+ readonly leading?: Signal<TemplateRef<WrOptionLeadingContext> | null>;
70
92
  }
71
93
  /**
72
94
  * Contract a `<wr-option>` uses to talk to its parent `<wr-select>`.
@@ -103,6 +125,17 @@ interface WrSelectContext {
103
125
  * `[serverSearch]`), in which case options must NOT self-hide.
104
126
  */
105
127
  readonly clientFilter: Signal<boolean>;
128
+ /**
129
+ * Whether the panel is open. An option draws its leading visual only while it
130
+ * is, because projected options exist — detached — for the whole life of the
131
+ * select, and a template instantiated there would be every avatar at once.
132
+ */
133
+ readonly panelOpen: Signal<boolean>;
134
+ /**
135
+ * The select-wide `wrOptionLeading` template — one declared as a direct child
136
+ * of `<wr-select>` — or `null`. An option with a template of its own ignores it.
137
+ */
138
+ readonly optionLeading: Signal<TemplateRef<WrOptionLeadingContext> | null>;
106
139
  /** Is the given option value currently selected? Handles both single and multi. */
107
140
  isSelected(value: unknown): boolean;
108
141
  /**
@@ -125,6 +158,8 @@ declare const WR_SELECT: InjectionToken<WrSelectContext>;
125
158
  interface SelectedChip {
126
159
  readonly value: unknown;
127
160
  readonly label: string;
161
+ /** The leading template this chip draws, or `null`. */
162
+ readonly leading: TemplateRef<WrOptionLeadingContext> | null;
128
163
  }
129
164
  /**
130
165
  * Combobox primitive. Renders a button that opens a CDK overlay
@@ -514,6 +549,33 @@ declare class WrSelect implements FormValueControl<unknown>, WrSelectContext {
514
549
  /** Id of the active option (for `aria-activedescendant`). */
515
550
  readonly activeOptionId: _angular_core.Signal<string | null>;
516
551
  protected readonly open: _angular_core.WritableSignal<boolean>;
552
+ /** WrSelectContext — whether the panel is open. Options gate their leading visual on it. */
553
+ readonly panelOpen: _angular_core.Signal<boolean>;
554
+ /**
555
+ * A `<ng-template wrOptionLeading>` declared as a DIRECT child of the select —
556
+ * `descendants: false`, so an option's own template is never mistaken for the
557
+ * select-wide default.
558
+ */
559
+ private readonly defaultLeading;
560
+ /**
561
+ * WrSelectContext — the select-wide leading template, or `null`. The only
562
+ * way to give a leading visual to the rows the select draws itself (`[options]`,
563
+ * `[loader]` results, virtual rows), and the default for projected options that
564
+ * declare none of their own.
565
+ */
566
+ readonly optionLeading: _angular_core.Signal<TemplateRef<WrOptionLeadingContext<unknown>> | null>;
567
+ /**
568
+ * The leading template for a value: the registered option's (its own, or the
569
+ * default it resolved to), else the select-wide default for a value with no
570
+ * registered option — a virtual row, or one a loader has since dropped.
571
+ */
572
+ private leadingFor;
573
+ /**
574
+ * The leading visual beside the selected value on a single-mode BUTTON trigger
575
+ * — one instance, for the one selected value. The search-shaped triggers do not
576
+ * draw one: their selection is an `<input>`'s text, which cannot hold markup.
577
+ */
578
+ protected readonly selectedLeading: _angular_core.Signal<TemplateRef<WrOptionLeadingContext<unknown>> | null>;
517
579
  /**
518
580
  * Trigger label for single mode. Multi reads `selectedChips` instead.
519
581
  *
@@ -768,19 +830,25 @@ declare class WrSelect implements FormValueControl<unknown>, WrSelectContext {
768
830
  private openOverlay;
769
831
  private closeOverlay;
770
832
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<WrSelect, never>;
771
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<WrSelect, "wr-select", never, { "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "clearLabel": { "alias": "clearLabel"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "readonly": { "alias": "readonly"; "required": false; "isSignal": true; }; "rounded": { "alias": "rounded"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "responsive": { "alias": "responsive"; "required": false; "isSignal": true; }; "mode": { "alias": "mode"; "required": false; "isSignal": true; }; "searchable": { "alias": "searchable"; "required": false; "isSignal": true; }; "searchQuery": { "alias": "searchQuery"; "required": false; "isSignal": true; }; "options": { "alias": "options"; "required": false; "isSignal": true; }; "displayWith": { "alias": "displayWith"; "required": false; "isSignal": true; }; "loader": { "alias": "loader"; "required": false; "isSignal": true; }; "serverSearch": { "alias": "serverSearch"; "required": false; "isSignal": true; }; "debounceMs": { "alias": "debounceMs"; "required": false; "isSignal": true; }; "minChars": { "alias": "minChars"; "required": false; "isSignal": true; }; "freeText": { "alias": "freeText"; "required": false; "isSignal": true; }; "virtualScroll": { "alias": "virtualScroll"; "required": false; "isSignal": true; }; "rowHeight": { "alias": "rowHeight"; "required": false; "isSignal": true; }; "viewportHeight": { "alias": "viewportHeight"; "required": false; "isSignal": true; }; "overscan": { "alias": "overscan"; "required": false; "isSignal": true; }; "noResultsText": { "alias": "noResultsText"; "required": false; "isSignal": true; }; "loadingText": { "alias": "loadingText"; "required": false; "isSignal": true; }; "loading": { "alias": "loading"; "required": false; "isSignal": true; }; "separators": { "alias": "separators"; "required": false; "isSignal": true; }; "allowDuplicates": { "alias": "allowDuplicates"; "required": false; "isSignal": true; }; "validate": { "alias": "validate"; "required": false; "isSignal": true; }; "clearable": { "alias": "clearable"; "required": false; "isSignal": true; }; "maxItems": { "alias": "maxItems"; "required": false; "isSignal": true; }; "maxTagCount": { "alias": "maxTagCount"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "searchQuery": "searchQueryChange"; "searchChange": "searchChange"; "value": "valueChange"; "touch": "touch"; }, ["projectedOptions"], ["*"], true, never>;
833
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<WrSelect, "wr-select", never, { "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "clearLabel": { "alias": "clearLabel"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "readonly": { "alias": "readonly"; "required": false; "isSignal": true; }; "rounded": { "alias": "rounded"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "responsive": { "alias": "responsive"; "required": false; "isSignal": true; }; "mode": { "alias": "mode"; "required": false; "isSignal": true; }; "searchable": { "alias": "searchable"; "required": false; "isSignal": true; }; "searchQuery": { "alias": "searchQuery"; "required": false; "isSignal": true; }; "options": { "alias": "options"; "required": false; "isSignal": true; }; "displayWith": { "alias": "displayWith"; "required": false; "isSignal": true; }; "loader": { "alias": "loader"; "required": false; "isSignal": true; }; "serverSearch": { "alias": "serverSearch"; "required": false; "isSignal": true; }; "debounceMs": { "alias": "debounceMs"; "required": false; "isSignal": true; }; "minChars": { "alias": "minChars"; "required": false; "isSignal": true; }; "freeText": { "alias": "freeText"; "required": false; "isSignal": true; }; "virtualScroll": { "alias": "virtualScroll"; "required": false; "isSignal": true; }; "rowHeight": { "alias": "rowHeight"; "required": false; "isSignal": true; }; "viewportHeight": { "alias": "viewportHeight"; "required": false; "isSignal": true; }; "overscan": { "alias": "overscan"; "required": false; "isSignal": true; }; "noResultsText": { "alias": "noResultsText"; "required": false; "isSignal": true; }; "loadingText": { "alias": "loadingText"; "required": false; "isSignal": true; }; "loading": { "alias": "loading"; "required": false; "isSignal": true; }; "separators": { "alias": "separators"; "required": false; "isSignal": true; }; "allowDuplicates": { "alias": "allowDuplicates"; "required": false; "isSignal": true; }; "validate": { "alias": "validate"; "required": false; "isSignal": true; }; "clearable": { "alias": "clearable"; "required": false; "isSignal": true; }; "maxItems": { "alias": "maxItems"; "required": false; "isSignal": true; }; "maxTagCount": { "alias": "maxTagCount"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "searchQuery": "searchQueryChange"; "searchChange": "searchChange"; "value": "valueChange"; "touch": "touch"; }, ["defaultLeading", "projectedOptions"], ["*"], true, never>;
772
834
  }
773
835
 
774
836
  /**
775
837
  * Single option inside a `<wr-select>`.
776
838
  *
777
- * The option's display label is taken from its projected text content;
778
- * the form value is its `value` input.
839
+ * The option's display label is taken from its projected text content — or
840
+ * from `label`, when that is set — and the form value is its `value` input. A
841
+ * leading visual goes in an `<ng-template wrOptionLeading>`, which keeps it out
842
+ * of the label and uninstantiated while the panel is closed.
779
843
  *
780
844
  * @example
781
845
  * ```html
782
846
  * <wr-option value="sm">Small</wr-option>
783
847
  * <wr-option [value]="42">Forty-two</wr-option>
848
+ * <wr-option [value]="p.id" [label]="p.name">
849
+ * <ng-template wrOptionLeading><wr-avatar shape="circle" size="1.5rem">{{ p.initials }}</wr-avatar></ng-template>
850
+ * {{ p.name }}
851
+ * </wr-option>
784
852
  * ```
785
853
  */
786
854
  declare class WrOption {
@@ -788,10 +856,40 @@ declare class WrOption {
788
856
  readonly value: _angular_core.InputSignal<unknown>;
789
857
  /** Disable this option. @default false */
790
858
  readonly disabled: _angular_core.InputSignalWithTransform<boolean, any>;
859
+ /**
860
+ * The text the select reports for this option — the trigger label, the chip,
861
+ * the chip's remove label and the search filter all read it. Unset (or empty),
862
+ * the option's own text is used, as it always was.
863
+ *
864
+ * Set it when the row shows more than the name: a projected email under a
865
+ * person, a count beside a category. It does NOT rename the row itself — the
866
+ * option's accessible name stays the text it draws, since a screen reader user
867
+ * browsing the list should hear what a sighted one reads there.
868
+ *
869
+ * @default null
870
+ */
871
+ readonly label: _angular_core.InputSignal<string | null>;
791
872
  /** Stable id used for `aria-activedescendant`. */
792
873
  readonly id: string;
793
874
  private readonly host;
794
875
  private readonly parent;
876
+ /** This option's own `<ng-template wrOptionLeading>`, if it declares one. */
877
+ private readonly ownLeading;
878
+ /**
879
+ * The leading template that applies to this option: its own, else the
880
+ * select-wide default. Registered with the parent so a chip can draw it.
881
+ */
882
+ private readonly leading;
883
+ /**
884
+ * The leading template to draw in THIS row right now — `null` while the panel
885
+ * is closed. A projected option is created with the page and lives, detached,
886
+ * for as long as the select does, so gating on the template alone would
887
+ * instantiate every row's avatar the moment the page renders. An option with
888
+ * no parent select has no panel to wait for.
889
+ */
890
+ protected readonly rowLeading: Signal<TemplateRef<WrOptionLeadingContext<unknown>> | null>;
891
+ /** The wrapper the leading visual renders into — skipped when reading the text. */
892
+ private readonly leadingSlot;
795
893
  /**
796
894
  * The option's own text, as a signal — the parent's trigger label and the
797
895
  * client-side filter both read it from here.
@@ -815,7 +913,12 @@ declare class WrOption {
815
913
  * @internal
816
914
  */
817
915
  private readonly labelText;
818
- private readonly label;
916
+ /**
917
+ * `label` when set, otherwise the option's own text. What the parent reads.
918
+ * Not `??`: an EMPTY label falls back too, since a blank trigger or a nameless
919
+ * chip is never what a `[label]="person.name"` on a missing name meant.
920
+ */
921
+ private readonly resolvedLabel;
819
922
  /**
820
923
  * @internal — true when this option is currently selected. Works for
821
924
  * both single and multi-select parents via `WrSelectContext.isSelected`.
@@ -825,7 +928,8 @@ declare class WrOption {
825
928
  protected readonly active: Signal<boolean>;
826
929
  /**
827
930
  * @internal — searchable selects only. True when the parent has a query that
828
- * the option's text content does not match (case-insensitive substring).
931
+ * the option's label does not match (case-insensitive substring) — `label`
932
+ * when set, the option's text otherwise, and never its leading visual.
829
933
  * Hidden options stay in the DOM so registration order survives but
830
934
  * collapse via CSS.
831
935
  */
@@ -834,13 +938,20 @@ declare class WrOption {
834
938
  constructor();
835
939
  /**
836
940
  * Read the projected text back off the DOM. Setting the same string again is
837
- * a signal no-op, so the render hook costs a `textContent` read and nothing
838
- * else on the passes where nothing moved.
941
+ * a signal no-op, so the render hook costs a short walk over the host's
942
+ * children and nothing else on the passes where nothing moved.
943
+ *
944
+ * A walk rather than the host's `textContent`, because the leading visual
945
+ * renders INSIDE the host while the panel is open: a `textContent` read turned
946
+ * an avatar's initials into the first word of the label, so the chip and the
947
+ * search filter read "ХР Хегай Роман". Only text and element children count —
948
+ * a comment's `textContent` is its data, and Angular anchors every
949
+ * `<ng-template>` with one.
839
950
  */
840
951
  private syncLabel;
841
952
  protected onClick(): void;
842
953
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<WrOption, never>;
843
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<WrOption, "wr-option", never, { "value": { "alias": "value"; "required": true; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
954
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<WrOption, "wr-option", never, { "value": { "alias": "value"; "required": true; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "label": { "alias": "label"; "required": false; "isSignal": true; }; }, {}, ["ownLeading"], ["*"], true, never>;
844
955
  }
845
956
 
846
957
  /**
@@ -866,5 +977,65 @@ declare class WrOptionGroup {
866
977
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<WrOptionGroup, "wr-option-group", never, { "label": { "alias": "label"; "required": true; "isSignal": true; }; }, {}, never, ["*"], true, never>;
867
978
  }
868
979
 
869
- export { WR_SELECT, WrOption, WrOptionGroup, WrSelect };
870
- export type { WrSelectContext, WrSelectMode, WrSelectSearchLoader, WrSelectSize, WrSelectTagValidator };
980
+ /**
981
+ * @license
982
+ *
983
+ * Use of this source code is governed by an MIT-style license that can be
984
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
985
+ */
986
+
987
+ /**
988
+ * A leading visual — an avatar, an icon, a colour dot — drawn before an
989
+ * option's label, and kept out of that label.
990
+ *
991
+ * It is a TEMPLATE, and that is the point. Projected `<wr-option>` children are
992
+ * created as soon as the page renders, panel open or not, so an avatar written
993
+ * straight into each option is 200 components and 200 image requests for an
994
+ * assignee picker nobody has opened. A template is only instantiated where it is
995
+ * drawn:
996
+ *
997
+ * - in an option row, while the panel is OPEN — destroyed again when it closes,
998
+ * and under `virtualScroll` only for the rows in the rendered window;
999
+ * - in a selected chip on a `mode="multi"` trigger, one per visible chip;
1000
+ * - beside the selected value on a single-mode BUTTON trigger — one copy. The
1001
+ * search-shaped triggers (`mode="search"`, `[searchable]`) do not draw it: their
1002
+ * selection is the text of an `<input>`, which cannot hold markup.
1003
+ *
1004
+ * Every copy sits in an `aria-hidden` wrapper and is skipped when the option's
1005
+ * text is read, so the trigger label, the chip, the search filter and the
1006
+ * option's accessible name all stay the option's words — never "ХР Хегай Роман".
1007
+ * Anything the visual says has to be said by the label as well.
1008
+ *
1009
+ * Two places to declare it. Inside a `<wr-option>` it belongs to that option.
1010
+ * As a DIRECT child of `<wr-select>` it is the default for every row, which is the
1011
+ * only way to reach rows the select draws itself — `[options]`, `[loader]`
1012
+ * results and virtual rows. An option's own template wins over the select's.
1013
+ *
1014
+ * @example
1015
+ * ```html
1016
+ * <wr-select mode="multi" placeholder="Assignees" [(value)]="assignees">
1017
+ * @for (p of people; track p.id) {
1018
+ * <wr-option [value]="p.id">
1019
+ * <ng-template wrOptionLeading let-placement="placement">
1020
+ * <wr-avatar shape="circle" [size]="placement === 'chip' ? 16 : 24" [url]="p.photo">{{ p.initials }}</wr-avatar>
1021
+ * </ng-template>
1022
+ * {{ p.name }}
1023
+ * </wr-option>
1024
+ * }
1025
+ * </wr-select>
1026
+ * ```
1027
+ */
1028
+ declare class WrOptionLeading {
1029
+ readonly template: TemplateRef<WrOptionLeadingContext<unknown>>;
1030
+ /**
1031
+ * Types the template's `let-` variables under `strictTemplates`. Without it
1032
+ * `let-placement` is `any`, and indexing a `Record<WrOptionLeadingPlacement, …>`
1033
+ * with it — the obvious way to size one visual per surface — does not compile.
1034
+ */
1035
+ static ngTemplateContextGuard(_dir: WrOptionLeading, _ctx: unknown): _ctx is WrOptionLeadingContext;
1036
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<WrOptionLeading, never>;
1037
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<WrOptionLeading, "ng-template[wrOptionLeading]", never, {}, {}, never, never, true, never>;
1038
+ }
1039
+
1040
+ export { WR_SELECT, WrOption, WrOptionGroup, WrOptionLeading, WrSelect };
1041
+ export type { WrOptionLeadingContext, WrOptionLeadingPlacement, WrSelectContext, WrSelectMode, WrSelectSearchLoader, WrSelectSize, WrSelectTagValidator };
@@ -16,7 +16,7 @@ import { InjectionToken } from '@angular/core';
16
16
  * Distinct from `Version` exported by `@angular/core`, which reports the
17
17
  * loaded Angular runtime version (e.g. `'22.0.0'`), not ngwr's.
18
18
  */
19
- declare const NGWR_VERSION = "14.5.1";
19
+ declare const NGWR_VERSION = "14.6.0";
20
20
 
21
21
  /**
22
22
  * @license