@spartan-ng/brain 0.0.1-alpha.412 → 0.0.1-alpha.413

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.
@@ -1,279 +1,100 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, ElementRef, PLATFORM_ID, computed, Directive, signal, Injectable, effect, input, booleanAttribute, DestroyRef, viewChild, contentChild, contentChildren, Component, ChangeDetectionStrategy, ChangeDetectorRef, output, NgModule } from '@angular/core';
3
- import * as i1 from '@angular/cdk/listbox';
4
- import { CdkOption, CdkListbox, CdkListboxModule } from '@angular/cdk/listbox';
5
- import { isPlatformBrowser, NgTemplateOutlet } from '@angular/common';
6
- import { toObservable, takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
7
- import { NgControl, NgForm, FormGroupDirective } from '@angular/forms';
8
- import { Subject, fromEvent, interval, of, combineLatest } from 'rxjs';
9
- import { skip, takeUntil, map, switchMap, delay } from 'rxjs/operators';
10
- import * as i1$1 from '@spartan-ng/brain/label';
2
+ import { InjectionToken, inject, ElementRef, input, booleanAttribute, computed, signal, Directive, DestroyRef, Injector, viewChild, contentChild, contentChildren, effect, afterNextRender, untracked, Component, ChangeDetectionStrategy, TemplateRef, PLATFORM_ID, numberAttribute, model, NgModule } from '@angular/core';
3
+ import { NgTemplateOutlet, isPlatformBrowser } from '@angular/common';
4
+ import { takeUntilDestroyed, toSignal, toObservable } from '@angular/core/rxjs-interop';
5
+ import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
6
+ import { Subject, fromEvent, interval, of } from 'rxjs';
7
+ import { takeUntil, switchMap, delay, map } from 'rxjs/operators';
8
+ import * as i1 from '@spartan-ng/brain/label';
11
9
  import { BrnLabelDirective } from '@spartan-ng/brain/label';
12
- import * as i1$2 from '@angular/cdk/overlay';
10
+ import { NgControl, NgForm, FormGroupDirective } from '@angular/forms';
11
+ import { CdkListboxModule } from '@angular/cdk/listbox';
12
+ import * as i1$1 from '@angular/cdk/overlay';
13
13
  import { CdkConnectedOverlay, OverlayModule } from '@angular/cdk/overlay';
14
14
  import { provideExposedSideProviderExisting, provideExposesStateProviderExisting } from '@spartan-ng/brain/core';
15
15
  import { BrnFormFieldControl } from '@spartan-ng/brain/form-field';
16
16
  import { ErrorStateMatcher, ErrorStateTracker } from '@spartan-ng/brain/forms';
17
17
 
18
- class BrnSelectTriggerDirective {
19
- _el = inject(ElementRef);
20
- _selectService = inject(BrnSelectService);
21
- _ngControl = inject(NgControl, { optional: true });
22
- _platform = inject(PLATFORM_ID);
23
- isExpanded = this._selectService.isExpanded;
24
- selectTriggerId = computed(() => `${this._selectService.id()}--trigger`);
25
- selectContentId = computed(() => `${this._selectService.id()}--content`);
26
- selectDisable = computed(() => this._selectService.disabled());
27
- selectTriggerLabelledBy = computed(() => {
28
- if (this._selectService.value() && this._selectService.value().length > 0) {
29
- return `${this._selectService.labelId()} ${this._selectService.id()}--value`;
30
- }
31
- return this._selectService.labelId();
32
- });
33
- _resizeObserver;
34
- constructor() {
35
- if (!this._selectService)
36
- return;
37
- this._selectService._setSelectTrigger(this);
38
- }
39
- ngAfterViewInit() {
40
- this._selectService.setTriggerWidth(this._el.nativeElement.offsetWidth);
41
- // if we are on the client, listen for element resize events
42
- if (isPlatformBrowser(this._platform)) {
43
- this._resizeObserver = new ResizeObserver(() => this._selectService.setTriggerWidth(this._el.nativeElement.offsetWidth));
44
- this._resizeObserver.observe(this._el.nativeElement);
45
- }
46
- }
47
- ngOnDestroy() {
48
- this._resizeObserver?.disconnect();
49
- }
50
- focus() {
51
- this._el.nativeElement.focus();
52
- }
53
- /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectTriggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
54
- /** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.5", type: BrnSelectTriggerDirective, isStandalone: true, selector: "[brnSelectTrigger]", host: { attributes: { "role": "combobox", "aria-autocomplete": "none", "type": "button" }, properties: { "attr.id": "selectTriggerId()", "disabled": "selectDisable()", "attr.aria-expanded": "isExpanded()", "attr.aria-controls": "selectContentId() + ''", "attr.aria-labelledBy": "selectTriggerLabelledBy()", "attr.dir": "_selectService.dir()", "class.ng-invalid": "this._ngControl?.invalid || null", "class.ng-dirty": "this._ngControl?.dirty || null", "class.ng-valid": "this._ngControl?.valid || null", "class.ng-touched": "this._ngControl?.touched || null", "class.ng-untouched": "this._ngControl?.untouched || null", "class.ng-pristine": "this._ngControl?.pristine || null" } }, ngImport: i0 });
18
+ const BrnSelectContentToken = new InjectionToken('BrnSelectContentToken');
19
+ function injectBrnSelectContent() {
20
+ return inject(BrnSelectContentToken);
55
21
  }
56
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectTriggerDirective, decorators: [{
57
- type: Directive,
58
- args: [{
59
- selector: '[brnSelectTrigger]',
60
- standalone: true,
61
- host: {
62
- role: 'combobox',
63
- '[attr.id]': 'selectTriggerId()',
64
- '[disabled]': 'selectDisable()',
65
- '[attr.aria-expanded]': 'isExpanded()',
66
- '[attr.aria-controls]': "selectContentId() + ''",
67
- '[attr.aria-labelledBy]': 'selectTriggerLabelledBy()',
68
- 'aria-autocomplete': 'none',
69
- '[attr.dir]': '_selectService.dir()',
70
- '[class.ng-invalid]': 'this._ngControl?.invalid || null',
71
- '[class.ng-dirty]': 'this._ngControl?.dirty || null',
72
- '[class.ng-valid]': 'this._ngControl?.valid || null',
73
- '[class.ng-touched]': 'this._ngControl?.touched || null',
74
- '[class.ng-untouched]': 'this._ngControl?.untouched || null',
75
- '[class.ng-pristine]': 'this._ngControl?.pristine || null',
76
- type: 'button',
77
- },
78
- }]
79
- }], ctorParameters: () => [] });
80
- class BrnSelectService {
81
- state = signal({
82
- id: '',
83
- labelId: '',
84
- panelId: '',
85
- isExpanded: false,
86
- placeholder: signal(''),
87
- multiple: signal(false),
88
- disabled: signal(false),
89
- disabledBySetDisabled: signal(false),
90
- dir: signal('ltr'),
91
- selectedOptions: [],
92
- possibleOptions: [],
93
- value: '',
94
- triggerWidth: 0,
95
- });
96
- id = computed(() => this.state().id);
97
- labelId = computed(() => this.state().labelId);
98
- panelId = computed(() => this.state().panelId);
99
- placeholder = computed(() => this.state().placeholder());
100
- disabled = computed(() => this.state().disabled() || this.state().disabledBySetDisabled());
101
- isExpanded = computed(() => this.state().isExpanded);
102
- multiple = computed(() => this.state().multiple());
103
- dir = computed(() => this.state().dir());
104
- selectedOptions = computed(() => this.state().selectedOptions);
105
- value = computed(() => this.state().value);
106
- triggerWidth = computed(() => this.state().triggerWidth);
107
- possibleOptions = computed(() => this.state().possibleOptions);
108
- _multiple$ = toObservable(this.multiple);
109
- listBoxValueChangeEvent$ = new Subject();
110
- _selectTrigger;
111
- get selectTrigger() {
112
- return this._selectTrigger;
113
- }
114
- constructor() {
115
- this.listBoxValueChangeEvent$.pipe(takeUntilDestroyed()).subscribe((listBoxChange) => {
116
- const updatedSelections = this.multiple() ? this.getUpdatedOptions(listBoxChange) : [listBoxChange.option];
117
- const value = this.multiple() ? listBoxChange.value : listBoxChange.value[0];
118
- this.state.update((state) => ({
119
- ...state,
120
- selectedOptions: [...updatedSelections],
121
- value: value,
122
- }));
123
- });
124
- // We need to skip the first value because we don't want to deselect all options when the component is initialized with a preselected value e.g. by the form control
125
- this._multiple$.pipe(skip(1), takeUntilDestroyed()).subscribe((multiple) => {
126
- if (!multiple && this.value().length > 1) {
127
- this.deselectAllOptions();
128
- }
129
- });
130
- }
131
- setTriggerWidth(triggerWidth) {
132
- this.state.update((s) => ({ ...s, triggerWidth }));
133
- }
134
- getUpdatedOptions(latestListboxChange) {
135
- const isNewSelection = latestListboxChange.value.findIndex((value) => value === latestListboxChange.option?.value);
136
- if (isNewSelection === -1) {
137
- const removedOptionIndex = this.selectedOptions().findIndex((option) => latestListboxChange.option === option);
138
- const options = this.selectedOptions();
139
- options.splice(removedOptionIndex, 1);
140
- return options;
141
- }
142
- return [...this.selectedOptions(), latestListboxChange.option];
143
- }
144
- deselectAllOptions() {
145
- this.state.update((state) => ({
146
- ...state,
147
- selectedOptions: [],
148
- value: [],
149
- }));
150
- }
151
- // Needed due to https://github.com/angular/angular/issues/20810
152
- _setSelectTrigger(trigger) {
153
- this._selectTrigger = trigger;
154
- }
155
- setInitialSelectedOptions(value) {
156
- this.selectOptionByValue(value);
157
- this.state.update((state) => ({
158
- ...state,
159
- value: value,
160
- initialSelectedOptions: this.selectedOptions(),
161
- selectedOptions: this.selectedOptions(),
162
- }));
163
- }
164
- /**
165
- * Sync the updated options with "possibleOptions" in the select service
166
- */
167
- updatePossibleOptions(options) {
168
- this.state.update((state) => ({
169
- ...state,
170
- possibleOptions: options,
171
- }));
172
- }
173
- selectOptionByValue(value) {
174
- const options = this.possibleOptions();
175
- if (value === null || value === undefined) {
176
- const nullOrUndefinedOption = options.find((o) => o && o.value === value);
177
- if (!nullOrUndefinedOption) {
178
- this.state.update((state) => ({
179
- ...state,
180
- selectedOptions: [],
181
- value: this.multiple() ? [] : '',
182
- }));
183
- return;
184
- }
185
- }
186
- if (this.multiple()) {
187
- const selectedOptions = options.filter((option) => {
188
- if (Array.isArray(value)) {
189
- return value.includes(option?.value);
190
- }
191
- return value === option?.value;
192
- });
193
- this.state.update((state) => ({
194
- ...state,
195
- selectedOptions,
196
- value: value,
197
- }));
198
- }
199
- else {
200
- const selectedOption = options.find((option) => option?.value === value);
201
- if (!selectedOption) {
202
- return;
203
- }
204
- this.state.update((state) => ({
205
- ...state,
206
- selectedOptions: [selectedOption],
207
- value: selectedOption.value,
208
- }));
209
- }
210
- }
211
- /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
212
- /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectService });
22
+ function provideBrnSelectContent(select) {
23
+ return { provide: BrnSelectContentToken, useExisting: select };
24
+ }
25
+
26
+ const BrnSelectToken = new InjectionToken('BrnSelectToken');
27
+ function injectBrnSelect() {
28
+ return inject(BrnSelectToken);
29
+ }
30
+ function provideBrnSelect(select) {
31
+ return { provide: BrnSelectToken, useExisting: select };
213
32
  }
214
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectService, decorators: [{
215
- type: Injectable
216
- }], ctorParameters: () => [] });
217
33
 
34
+ let nextId$1 = 0;
218
35
  class BrnSelectOptionDirective {
219
- _cdkSelectOption = inject(CdkOption, { host: true });
220
- _selectService = inject(BrnSelectService);
221
- _focused = signal(false);
36
+ _select = injectBrnSelect();
37
+ _content = injectBrnSelectContent();
222
38
  elementRef = inject(ElementRef);
223
- selected = computed(() => {
224
- if (Array.isArray(this._selectService.value())) {
225
- const itemFound = this._selectService.value().find((val) => val === this._cdkSelectOption.value);
226
- return !!itemFound;
227
- }
228
- return this._cdkSelectOption.value === this._selectService.value();
39
+ id = input(`brn-option-${nextId$1++}`);
40
+ value = input();
41
+ // we use "_disabled" here because disabled is already defined in the Highlightable interface
42
+ _disabled = input(false, {
43
+ alias: 'disabled',
44
+ transform: booleanAttribute,
229
45
  });
230
- focused = computed(() => this._focused());
46
+ get disabled() {
47
+ return this._disabled();
48
+ }
49
+ selected = computed(() => this.value() !== undefined && this._select.isSelected(this.value()));
50
+ _active = signal(false);
231
51
  checkedState = computed(() => (this.selected() ? 'checked' : 'unchecked'));
232
- dir = computed(() => this._selectService.dir());
233
- constructor() {
234
- effect(() => {
235
- this._cdkSelectOption.value = this.value();
236
- });
237
- effect(() => {
238
- this._cdkSelectOption.disabled = this.disabledSignal();
239
- });
52
+ dir = this._select.dir;
53
+ select() {
54
+ if (this._disabled()) {
55
+ return;
56
+ }
57
+ this._select.selectOption(this.value());
240
58
  }
241
- ngAfterContentChecked() {
242
- this._cdkSelectOption.value = this.value();
59
+ /** Get the label for this element which is required by the FocusableOption interface. */
60
+ getLabel() {
61
+ return this.elementRef.nativeElement.textContent?.trim() ?? '';
243
62
  }
244
- value = input(null);
245
- // we use "disabledSignal" here because disabled is already defined in the FocusableOption interface
246
- disabledSignal = input(false, {
247
- alias: 'disabled',
248
- transform: booleanAttribute,
249
- });
250
- hover() {
251
- this.focus();
63
+ setActiveStyles() {
64
+ this._active.set(true);
65
+ // scroll the option into view if it is not visible
66
+ this.elementRef.nativeElement.scrollIntoView({ block: 'nearest' });
252
67
  }
253
- focus() {
254
- this._cdkSelectOption.focus();
255
- this._focused.set(true);
68
+ setInactiveStyles() {
69
+ this._active.set(false);
256
70
  }
257
- blur() {
258
- this._focused.set(false);
71
+ activate() {
72
+ if (this._disabled()) {
73
+ return;
74
+ }
75
+ this._content.setActiveOption(this);
259
76
  }
260
77
  /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectOptionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
261
- /** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.2.5", type: BrnSelectOptionDirective, isStandalone: true, selector: "[brnOption]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabledSignal: { classPropertyName: "disabledSignal", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "mouseenter": "hover()", "blur": "blur()" }, properties: { "attr.dir": "_selectService.dir()", "attr.data-disabled": "disabledSignal() ? '' : undefined" } }, hostDirectives: [{ directive: i1.CdkOption }], ngImport: i0 });
78
+ /** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.2.5", type: BrnSelectOptionDirective, isStandalone: true, selector: "[brnOption]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, _disabled: { classPropertyName: "_disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "option" }, listeners: { "click": "select()", "mouseenter": "activate()" }, properties: { "id": "id()", "attr.aria-selected": "selected()", "attr.aria-disabled": "_disabled()", "attr.dir": "_select.dir()", "attr.data-active": "_active() ? '' : undefined", "attr.data-disabled": "_disabled() ? '' : undefined" } }, ngImport: i0 });
262
79
  }
263
80
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectOptionDirective, decorators: [{
264
81
  type: Directive,
265
82
  args: [{
266
83
  selector: '[brnOption]',
267
84
  standalone: true,
268
- hostDirectives: [CdkOption],
269
85
  host: {
270
- '(mouseenter)': 'hover()',
271
- '(blur)': 'blur()',
272
- '[attr.dir]': '_selectService.dir()',
273
- '[attr.data-disabled]': "disabledSignal() ? '' : undefined",
86
+ role: 'option',
87
+ '[id]': 'id()',
88
+ '[attr.aria-selected]': 'selected()',
89
+ '[attr.aria-disabled]': '_disabled()',
90
+ '(click)': 'select()',
91
+ '[attr.dir]': '_select.dir()',
92
+ '[attr.data-active]': "_active() ? '' : undefined",
93
+ '[attr.data-disabled]': "_disabled() ? '' : undefined",
94
+ '(mouseenter)': 'activate()',
274
95
  },
275
96
  }]
276
- }], ctorParameters: () => [] });
97
+ }] });
277
98
 
278
99
  const SCROLLBY_PIXELS = 100;
279
100
  class BrnSelectScrollUpDirective {
@@ -333,51 +154,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImpor
333
154
  }]
334
155
  }] });
335
156
  class BrnSelectContentComponent {
336
- _el = inject(ElementRef);
337
- _cdkListbox = inject(CdkListbox, { host: true });
338
- _destroyRef = inject(DestroyRef);
339
- _selectService = inject(BrnSelectService);
340
- labelledBy = this._selectService.labelId;
341
- id = this._selectService.id;
157
+ _elementRef = inject(ElementRef);
158
+ _injector = inject(Injector);
159
+ _select = injectBrnSelect();
342
160
  canScrollUp = signal(false);
343
161
  canScrollDown = signal(false);
344
- initialSelectedOptions$ = toObservable(this._selectService.selectedOptions);
345
162
  viewport = viewChild.required('viewport');
346
163
  scrollUpBtn = contentChild(BrnSelectScrollUpDirective);
347
164
  scrollDownBtn = contentChild(BrnSelectScrollDownDirective);
348
165
  _options = contentChildren(BrnSelectOptionDirective, { descendants: true });
166
+ /** @internal */
167
+ keyManager = null;
349
168
  constructor() {
350
- this._cdkListbox.valueChange
351
- .asObservable()
352
- .pipe(takeUntilDestroyed())
353
- .subscribe((val) => this._selectService.listBoxValueChangeEvent$.next(val));
354
169
  effect(() => {
355
- this._cdkListbox.multiple = this._selectService.multiple();
356
- this._selectService.isExpanded() && setTimeout(() => this.updateArrowDisplay());
170
+ this._select.open() && afterNextRender(() => this.updateArrowDisplay(), { injector: this._injector });
357
171
  });
358
172
  }
359
- ngAfterViewInit() {
360
- this.setInitiallySelectedOptions();
361
- }
362
- setInitiallySelectedOptions() {
363
- this.initialSelectedOptions$.pipe(takeUntilDestroyed(this._destroyRef)).subscribe((selectedOptions) => {
364
- // Reapplying cdkLibstbox multiple because seems this is running before effect that
365
- // updates cdklistbox, reapplying multiple true so we can set the multiple initial options
366
- if (this._selectService.multiple()) {
367
- this._cdkListbox.multiple = true;
173
+ ngAfterContentInit() {
174
+ this.keyManager = new ActiveDescendantKeyManager(this._options, this._injector)
175
+ .withHomeAndEnd()
176
+ .withVerticalOrientation()
177
+ .withTypeAhead()
178
+ .withAllowedModifierKeys(['shiftKey'])
179
+ .withWrap()
180
+ .skipPredicate((option) => option._disabled());
181
+ effect(() => {
182
+ // any time the select is opened, we need to focus the first selected option or the first option
183
+ const open = this._select.open();
184
+ const options = this._options();
185
+ if (!open || !options.length) {
186
+ return;
368
187
  }
369
- for (const cdkOption of this._selectService.possibleOptions()) {
370
- if (selectedOptions.includes(cdkOption)) {
371
- cdkOption?.select();
188
+ untracked(() => {
189
+ const selectedOption = options.find((option) => option.selected());
190
+ if (selectedOption) {
191
+ this.keyManager?.setActiveItem(selectedOption);
372
192
  }
373
193
  else {
374
- cdkOption?.deselect();
194
+ this.keyManager?.setFirstItemActive();
375
195
  }
376
- }
377
- for (const cdkOption of selectedOptions) {
378
- cdkOption?.select();
379
- }
380
- });
196
+ });
197
+ }, { injector: this._injector });
381
198
  }
382
199
  updateArrowDisplay() {
383
200
  const { scrollTop, scrollHeight, clientHeight } = this.viewport().nativeElement;
@@ -389,7 +206,7 @@ class BrnSelectContentComponent {
389
206
  this.updateArrowDisplay();
390
207
  }
391
208
  focusList() {
392
- this._cdkListbox.focus();
209
+ this._elementRef.nativeElement.focus();
393
210
  }
394
211
  moveFocusUp() {
395
212
  this.viewport().nativeElement.scrollBy({ top: -SCROLLBY_PIXELS, behavior: 'smooth' });
@@ -399,15 +216,29 @@ class BrnSelectContentComponent {
399
216
  }
400
217
  moveFocusDown() {
401
218
  this.viewport().nativeElement.scrollBy({ top: SCROLLBY_PIXELS, behavior: 'smooth' });
402
- const viewportSize = this._el.nativeElement.scrollHeight;
219
+ const viewportSize = this._elementRef.nativeElement.scrollHeight;
403
220
  const viewportScrollPosition = this.viewport().nativeElement.scrollTop;
404
221
  if (viewportSize + viewportScrollPosition + SCROLLBY_PIXELS >
405
222
  this.viewport().nativeElement.scrollHeight + SCROLLBY_PIXELS / 2) {
406
223
  this.scrollDownBtn()?.stopEmittingEvents();
407
224
  }
408
225
  }
226
+ setActiveOption(option) {
227
+ const index = this._options().findIndex((o) => o === option);
228
+ if (index === -1) {
229
+ return;
230
+ }
231
+ this.keyManager?.setActiveItem(index);
232
+ }
233
+ selectActiveItem(event) {
234
+ event.preventDefault();
235
+ const activeOption = this.keyManager?.activeItem;
236
+ if (activeOption) {
237
+ this._select.selectOption(activeOption.value());
238
+ }
239
+ }
409
240
  /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
410
- /** @nocollapse */ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "18.2.5", type: BrnSelectContentComponent, isStandalone: true, selector: "brn-select-content, hlm-select-content:not(noHlm)", host: { properties: { "attr.aria-labelledBy": "labelledBy()", "attr.aria-controlledBy": "id() +'--trigger'", "id": "id() + '--content'", "attr.dir": "_selectService.dir()" } }, queries: [{ propertyName: "scrollUpBtn", first: true, predicate: BrnSelectScrollUpDirective, descendants: true, isSignal: true }, { propertyName: "scrollDownBtn", first: true, predicate: BrnSelectScrollDownDirective, descendants: true, isSignal: true }, { propertyName: "_options", predicate: BrnSelectOptionDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "viewport", first: true, predicate: ["viewport"], descendants: true, isSignal: true }], hostDirectives: [{ directive: i1.CdkListbox }], ngImport: i0, template: `
241
+ /** @nocollapse */ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "18.2.5", type: BrnSelectContentComponent, isStandalone: true, selector: "brn-select-content, hlm-select-content:not(noHlm)", host: { attributes: { "role": "listbox", "tabindex": "0", "aria-orientation": "vertical" }, listeners: { "keydown": "keyManager?.onKeydown($event)", "keydown.enter": "selectActiveItem($event)", "keydown.space": "selectActiveItem($event)" }, properties: { "attr.aria-multiselectable": "_select.multiple()", "attr.aria-disabled": "_select.disabled() || _select._formDisabled()", "attr.aria-activedescendant": "keyManager?.activeItem?.id()", "attr.aria-labelledBy": "_select.labelId()", "attr.aria-controlledBy": "_select.id() +'--trigger'", "id": "_select.id() + '--content'", "attr.dir": "_select.dir()" } }, providers: [provideBrnSelectContent(BrnSelectContentComponent)], queries: [{ propertyName: "scrollUpBtn", first: true, predicate: BrnSelectScrollUpDirective, descendants: true, isSignal: true }, { propertyName: "scrollDownBtn", first: true, predicate: BrnSelectScrollDownDirective, descendants: true, isSignal: true }, { propertyName: "_options", predicate: BrnSelectOptionDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "viewport", first: true, predicate: ["viewport"], descendants: true, isSignal: true }], ngImport: i0, template: `
411
242
  <ng-template #scrollUp>
412
243
  <ng-content select="hlm-select-scroll-up" />
413
244
  <ng-content select="brnSelectScrollUp" />
@@ -436,11 +267,20 @@ class BrnSelectContentComponent {
436
267
  }
437
268
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectContentComponent, decorators: [{
438
269
  type: Component,
439
- args: [{ selector: 'brn-select-content, hlm-select-content:not(noHlm)', standalone: true, imports: [BrnSelectScrollUpDirective, BrnSelectScrollDownDirective, NgTemplateOutlet], hostDirectives: [CdkListbox], changeDetection: ChangeDetectionStrategy.OnPush, host: {
440
- '[attr.aria-labelledBy]': 'labelledBy()',
441
- '[attr.aria-controlledBy]': "id() +'--trigger'",
442
- '[id]': "id() + '--content'",
443
- '[attr.dir]': '_selectService.dir()',
270
+ args: [{ standalone: true, selector: 'brn-select-content, hlm-select-content:not(noHlm)', imports: [NgTemplateOutlet], providers: [provideBrnSelectContent(BrnSelectContentComponent)], changeDetection: ChangeDetectionStrategy.OnPush, host: {
271
+ role: 'listbox',
272
+ tabindex: '0',
273
+ '[attr.aria-multiselectable]': '_select.multiple()',
274
+ '[attr.aria-disabled]': '_select.disabled() || _select._formDisabled()',
275
+ 'aria-orientation': 'vertical',
276
+ '[attr.aria-activedescendant]': 'keyManager?.activeItem?.id()',
277
+ '[attr.aria-labelledBy]': '_select.labelId()',
278
+ '[attr.aria-controlledBy]': "_select.id() +'--trigger'",
279
+ '[id]': "_select.id() + '--content'",
280
+ '[attr.dir]': '_select.dir()',
281
+ '(keydown)': 'keyManager?.onKeydown($event)',
282
+ '(keydown.enter)': 'selectActiveItem($event)',
283
+ '(keydown.space)': 'selectActiveItem($event)',
444
284
  }, template: `
445
285
  <ng-template #scrollUp>
446
286
  <ng-content select="hlm-select-scroll-up" />
@@ -493,7 +333,7 @@ class BrnSelectLabelDirective {
493
333
  this._group?.labelledBy.set(this._label.id());
494
334
  }
495
335
  /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectLabelDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
496
- /** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.5", type: BrnSelectLabelDirective, isStandalone: true, selector: "[brnSelectLabel]", hostDirectives: [{ directive: i1$1.BrnLabelDirective }], ngImport: i0 });
336
+ /** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.5", type: BrnSelectLabelDirective, isStandalone: true, selector: "[brnSelectLabel]", hostDirectives: [{ directive: i1.BrnLabelDirective }], ngImport: i0 });
497
337
  }
498
338
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectLabelDirective, decorators: [{
499
339
  type: Directive,
@@ -504,71 +344,195 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImpor
504
344
  }]
505
345
  }], ctorParameters: () => [] });
506
346
 
507
- class BrnSelectValueComponent {
508
- _selectService = inject(BrnSelectService);
509
- id = computed(() => `${this._selectService.id()}--value`);
510
- placeholder = computed(() => this._selectService.placeholder());
511
- value = null;
512
- transformFn = input((values) => (values ?? []).join(', '));
347
+ class BrnSelectPlaceholderDirective {
348
+ /** @internale */
349
+ templateRef = inject(TemplateRef);
350
+ /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectPlaceholderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
351
+ /** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.5", type: BrnSelectPlaceholderDirective, isStandalone: true, selector: "[brnSelectPlaceholder], [hlmSelectPlaceholder]", ngImport: i0 });
352
+ }
353
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectPlaceholderDirective, decorators: [{
354
+ type: Directive,
355
+ args: [{
356
+ standalone: true,
357
+ selector: '[brnSelectPlaceholder], [hlmSelectPlaceholder]',
358
+ }]
359
+ }] });
360
+
361
+ class BrnSelectTriggerDirective {
362
+ _elementRef = inject(ElementRef);
363
+ _select = injectBrnSelect();
364
+ _ngControl = inject(NgControl, { optional: true });
365
+ _platform = inject(PLATFORM_ID);
366
+ _triggerId = computed(() => `${this._select.id()}--trigger`);
367
+ _contentId = computed(() => `${this._select.id()}--content`);
368
+ _disabled = computed(() => this._select.disabled() || this._select._formDisabled());
369
+ _labelledBy = computed(() => {
370
+ const value = this._select.value();
371
+ if (Array.isArray(value) && value.length > 0) {
372
+ return `${this._select.labelId()} ${this._select.id()}--value`;
373
+ }
374
+ return this._select.labelId();
375
+ });
376
+ _resizeObserver;
513
377
  constructor() {
514
- const cdr = inject(ChangeDetectorRef);
515
- // In certain cases (when using a computed signal for value) where the value of the select and the options are
516
- // changed dynamically, the template does not update until the next frame. To work around this we can use a simple
517
- // string variable in the template and manually trigger change detection when we update it.
518
- toObservable(this._selectService.selectedOptions)
519
- .pipe(takeUntilDestroyed())
520
- .subscribe((value) => {
521
- if (value.length === 0) {
522
- this.value = null;
523
- cdr.detectChanges();
524
- return;
525
- }
526
- const selectedLabels = value.map((selectedOption) => selectedOption?.getLabel());
527
- if (this._selectService.dir() === 'rtl') {
528
- selectedLabels.reverse();
529
- }
530
- const result = this.transformFn()(selectedLabels);
531
- this.value = result;
532
- cdr.detectChanges();
533
- });
378
+ this._select.trigger.set(this);
379
+ }
380
+ ngAfterViewInit() {
381
+ this._select.triggerWidth.set(this._elementRef.nativeElement.offsetWidth);
382
+ // if we are on the client, listen for element resize events
383
+ if (isPlatformBrowser(this._platform)) {
384
+ this._resizeObserver = new ResizeObserver(() => this._select.triggerWidth.set(this._elementRef.nativeElement.offsetWidth));
385
+ this._resizeObserver.observe(this._elementRef.nativeElement);
386
+ }
534
387
  }
388
+ ngOnDestroy() {
389
+ this._resizeObserver?.disconnect();
390
+ }
391
+ focus() {
392
+ this._elementRef.nativeElement.focus();
393
+ }
394
+ /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectTriggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
395
+ /** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.5", type: BrnSelectTriggerDirective, isStandalone: true, selector: "[brnSelectTrigger]", host: { attributes: { "type": "button", "role": "combobox", "aria-autocomplete": "none" }, listeners: { "keydown.ArrowDown": "_select.show()" }, properties: { "attr.id": "_triggerId()", "disabled": "_disabled()", "attr.aria-expanded": "_select.open()", "attr.aria-controls": "_contentId()", "attr.aria-labelledBy": "_labelledBy()", "attr.dir": "_select.dir()", "class.ng-invalid": "_ngControl?.invalid || null", "class.ng-dirty": "_ngControl?.dirty || null", "class.ng-valid": "_ngControl?.valid || null", "class.ng-touched": "_ngControl?.touched || null", "class.ng-untouched": "_ngControl?.untouched || null", "class.ng-pristine": "_ngControl?.pristine || null" } }, ngImport: i0 });
396
+ }
397
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectTriggerDirective, decorators: [{
398
+ type: Directive,
399
+ args: [{
400
+ selector: '[brnSelectTrigger]',
401
+ standalone: true,
402
+ host: {
403
+ type: 'button',
404
+ role: 'combobox',
405
+ '[attr.id]': '_triggerId()',
406
+ '[disabled]': '_disabled()',
407
+ '[attr.aria-expanded]': '_select.open()',
408
+ '[attr.aria-controls]': '_contentId()',
409
+ '[attr.aria-labelledBy]': '_labelledBy()',
410
+ 'aria-autocomplete': 'none',
411
+ '[attr.dir]': '_select.dir()',
412
+ '[class.ng-invalid]': '_ngControl?.invalid || null',
413
+ '[class.ng-dirty]': '_ngControl?.dirty || null',
414
+ '[class.ng-valid]': '_ngControl?.valid || null',
415
+ '[class.ng-touched]': '_ngControl?.touched || null',
416
+ '[class.ng-untouched]': '_ngControl?.untouched || null',
417
+ '[class.ng-pristine]': '_ngControl?.pristine || null',
418
+ '(keydown.ArrowDown)': '_select.show()',
419
+ },
420
+ }]
421
+ }], ctorParameters: () => [] });
422
+
423
+ class BrnSelectValueDirective {
424
+ /** @internale */
425
+ templateRef = inject(TemplateRef);
426
+ /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectValueDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
427
+ /** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.5", type: BrnSelectValueDirective, isStandalone: true, selector: "[brnSelectValue], [hlmSelectValue]", ngImport: i0 });
428
+ }
429
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectValueDirective, decorators: [{
430
+ type: Directive,
431
+ args: [{
432
+ standalone: true,
433
+ selector: '[brnSelectValue], [hlmSelectValue]',
434
+ }]
435
+ }] });
436
+
437
+ class BrnSelectValueComponent {
438
+ _select = injectBrnSelect();
439
+ id = computed(() => `${this._select.id()}--value`);
440
+ placeholder = computed(() => this._select.placeholder());
441
+ _showPlaceholder = computed(() => this.value() === null || this.value() === undefined || this.value() === '');
442
+ /** Allow a custom value template */
443
+ customValueTemplate = contentChild(BrnSelectValueDirective, { descendants: true });
444
+ customPlaceholderTemplate = contentChild(BrnSelectPlaceholderDirective, { descendants: true });
445
+ value = computed(() => {
446
+ const value = this._values();
447
+ if (value.length === 0) {
448
+ return null;
449
+ }
450
+ // remove any selected values that are not in the options list
451
+ const existingOptions = value.filter((val) => this._select.options().some((option) => option.value() === val));
452
+ const selectedOption = existingOptions.map((val) => this._select.options().find((option) => option.value() === val));
453
+ if (selectedOption.length === 0) {
454
+ return null;
455
+ }
456
+ const selectedLabels = selectedOption.map((option) => option?.getLabel());
457
+ if (this._select.dir() === 'rtl') {
458
+ selectedLabels.reverse();
459
+ }
460
+ return this.transformFn()(selectedLabels);
461
+ });
462
+ /** Normalize the values as an array */
463
+ _values = computed(() => Array.isArray(this._select.value()) ? this._select.value() : [this._select.value()]);
464
+ transformFn = input((values) => (values ?? []).join(', '));
535
465
  /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectValueComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
536
- /** @nocollapse */ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "18.2.5", type: BrnSelectValueComponent, isStandalone: true, selector: "brn-select-value, hlm-select-value", inputs: { transformFn: { classPropertyName: "transformFn", publicName: "transformFn", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "id()" } }, ngImport: i0, template: `
537
- {{ value || placeholder() }}
538
- `, isInline: true, styles: [":host{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1;white-space:nowrap;pointer-events:none}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
466
+ /** @nocollapse */ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.5", type: BrnSelectValueComponent, isStandalone: true, selector: "brn-select-value, hlm-select-value", inputs: { transformFn: { classPropertyName: "transformFn", publicName: "transformFn", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "id()" } }, queries: [{ propertyName: "customValueTemplate", first: true, predicate: BrnSelectValueDirective, descendants: true, isSignal: true }, { propertyName: "customPlaceholderTemplate", first: true, predicate: BrnSelectPlaceholderDirective, descendants: true, isSignal: true }], ngImport: i0, template: `
467
+ @if (_showPlaceholder()) {
468
+ <ng-container [ngTemplateOutlet]="customPlaceholderTemplate()?.templateRef ?? defaultPlaceholderTemplate" />
469
+ } @else {
470
+ <ng-container
471
+ [ngTemplateOutlet]="customValueTemplate()?.templateRef ?? defaultValueTemplate"
472
+ [ngTemplateOutletContext]="{ $implicit: _select.value() }"
473
+ />
474
+ }
475
+
476
+ <ng-template #defaultValueTemplate>{{ value() }}</ng-template>
477
+ <ng-template #defaultPlaceholderTemplate>{{ placeholder() }}</ng-template>
478
+ `, isInline: true, styles: [":host{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1;white-space:nowrap;pointer-events:none}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
539
479
  }
540
480
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectValueComponent, decorators: [{
541
481
  type: Component,
542
- args: [{ selector: 'brn-select-value, hlm-select-value', template: `
543
- {{ value || placeholder() }}
482
+ args: [{ selector: 'brn-select-value, hlm-select-value', imports: [NgTemplateOutlet], template: `
483
+ @if (_showPlaceholder()) {
484
+ <ng-container [ngTemplateOutlet]="customPlaceholderTemplate()?.templateRef ?? defaultPlaceholderTemplate" />
485
+ } @else {
486
+ <ng-container
487
+ [ngTemplateOutlet]="customValueTemplate()?.templateRef ?? defaultValueTemplate"
488
+ [ngTemplateOutletContext]="{ $implicit: _select.value() }"
489
+ />
490
+ }
491
+
492
+ <ng-template #defaultValueTemplate>{{ value() }}</ng-template>
493
+ <ng-template #defaultPlaceholderTemplate>{{ placeholder() }}</ng-template>
544
494
  `, host: {
545
495
  '[id]': 'id()',
546
496
  }, standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1;white-space:nowrap;pointer-events:none}\n"] }]
547
- }], ctorParameters: () => [] });
497
+ }] });
548
498
 
549
499
  let nextId = 0;
550
500
  class BrnSelectComponent {
551
- _selectService = inject(BrnSelectService);
552
- triggerWidth = this._selectService.triggerWidth;
553
- multiple = input(false);
501
+ _defaultErrorStateMatcher = inject(ErrorStateMatcher);
502
+ _parentForm = inject(NgForm, { optional: true });
503
+ _injector = inject(Injector);
504
+ _parentFormGroup = inject(FormGroupDirective, { optional: true });
505
+ ngControl = inject(NgControl, { optional: true, self: true });
506
+ id = input(`brn-select-${nextId++}`);
507
+ multiple = input(false, {
508
+ transform: booleanAttribute,
509
+ });
554
510
  placeholder = input('');
555
- disabled = input(false);
511
+ disabled = input(false, {
512
+ transform: booleanAttribute,
513
+ });
556
514
  dir = input('ltr');
557
- _disabledFromSetDisabledState = signal(false);
515
+ closeDelay = input(100, {
516
+ transform: numberAttribute,
517
+ });
518
+ open = model(false);
519
+ value = model();
520
+ compareWith = input((o1, o2) => o1 === o2);
521
+ _formDisabled = signal(false);
522
+ /** Label provided by the consumer. */
558
523
  selectLabel = contentChild(BrnLabelDirective, { descendants: false });
559
524
  /** Overlay pane containing the options. */
560
525
  selectContent = contentChild.required(BrnSelectContentComponent);
561
- options = contentChildren(CdkOption, { descendants: true });
562
- options$ = toObservable(this.options);
563
- optionsAndIndex$ = this.options$.pipe(map((options, index) => [options, index]));
526
+ /** @internal */
527
+ options = contentChildren(BrnSelectOptionDirective, { descendants: true });
528
+ /** @internal Derive the selected options to filter out the unselected options */
529
+ selectedOptions = computed(() => this.options().filter((option) => option.selected()));
564
530
  /** Overlay pane containing the options. */
565
- _overlayDir = viewChild(CdkConnectedOverlay);
566
- closeDelay = input(100);
567
- isExpanded = this._selectService.isExpanded;
568
- _delayedExpanded = toSignal(toObservable(this.isExpanded).pipe(switchMap((expanded) => (!expanded ? of(expanded).pipe(delay(this.closeDelay())) : of(expanded))), takeUntilDestroyed()), { initialValue: false });
569
- state = computed(() => (this.isExpanded() ? 'open' : 'closed'));
570
- openedChange = output();
571
- valueChange = output();
531
+ _overlayDir = viewChild.required(CdkConnectedOverlay);
532
+ trigger = signal(null);
533
+ triggerWidth = signal(0);
534
+ _delayedExpanded = toSignal(toObservable(this.open).pipe(switchMap((expanded) => (!expanded ? of(expanded).pipe(delay(this.closeDelay())) : of(expanded))), takeUntilDestroyed()), { initialValue: false });
535
+ state = computed(() => (this.open() ? 'open' : 'closed'));
572
536
  _positionChanges$ = new Subject();
573
537
  side = toSignal(this._positionChanges$.pipe(map((change) =>
574
538
  // todo: better translation or adjusting hlm to take that into account
@@ -577,14 +541,11 @@ class BrnSelectComponent {
577
541
  ? 'left'
578
542
  : 'right'
579
543
  : change.connectionPair.originY)), { initialValue: 'bottom' });
580
- backupLabelId = computed(() => this._selectService.labelId());
581
- labelProvided = signal(false);
582
- ngControl = inject(NgControl, { optional: true, self: true });
544
+ labelId = computed(() => this.selectLabel()?.id ?? `${this.id()}--label`);
583
545
  // eslint-disable-next-line @typescript-eslint/no-empty-function
584
546
  _onChange = () => { };
585
547
  // eslint-disable-next-line @typescript-eslint/no-empty-function
586
548
  _onTouched = () => { };
587
- _shouldEmitValueChange = signal(false);
588
549
  /*
589
550
  * This position config ensures that the top "start" corner of the overlay
590
551
  * is aligned with with the top "start" of the origin by default (overlapping
@@ -618,113 +579,41 @@ class BrnSelectComponent {
618
579
  },
619
580
  ];
620
581
  errorStateTracker;
621
- _defaultErrorStateMatcher = inject(ErrorStateMatcher);
622
- _parentForm = inject(NgForm, { optional: true });
623
- _parentFormGroup = inject(FormGroupDirective, { optional: true });
624
582
  errorState = computed(() => this.errorStateTracker.errorState());
625
- writeValue$ = new Subject();
626
583
  constructor() {
627
- this._selectService.state.update((state) => ({
628
- ...state,
629
- multiple: this.multiple,
630
- placeholder: this.placeholder,
631
- disabled: this.disabled,
632
- disabledBySetDisabled: this._disabledFromSetDisabledState,
633
- dir: this.dir,
634
- }));
635
- this.handleOptionChanges();
636
- this.handleInitialOptionSelect();
637
- this._selectService.state.update((state) => ({
638
- ...state,
639
- id: `brn-select-${nextId++}`,
640
- }));
641
584
  if (this.ngControl !== null) {
642
585
  this.ngControl.valueAccessor = this;
643
586
  }
644
- // Watch for Listbox Selection Changes to trigger Collapse and Value Change
645
- this._selectService.listBoxValueChangeEvent$.pipe(takeUntilDestroyed()).subscribe(() => {
646
- if (!this.multiple()) {
647
- this.close();
648
- }
649
- // we set shouldEmitValueChange to true because we want to propagate the value change
650
- // as a result of user interaction
651
- this._shouldEmitValueChange.set(true);
652
- });
653
- /**
654
- * Listening to value changes in order to trigger forms api on change
655
- * ShouldEmitValueChange simply ensures we only propagate value change when a user makes a selection
656
- * we don't propagate changes made from outside the component (ex. patch value or initial value from form control)
657
- */
658
- toObservable(this._selectService.value).subscribe((value) => {
659
- if (this._shouldEmitValueChange()) {
660
- this._onChange((value ?? null));
661
- this.valueChange.emit((value ?? null));
662
- }
663
- this._shouldEmitValueChange.set(true);
664
- });
665
587
  this.errorStateTracker = new ErrorStateTracker(this._defaultErrorStateMatcher, this.ngControl, this._parentFormGroup, this._parentForm);
666
588
  }
667
- ngAfterContentInit() {
668
- // Check if Label Directive Provided and pass to service
669
- const label = this.selectLabel();
670
- if (label) {
671
- this.labelProvided.set(true);
672
- this._selectService.state.update((state) => ({
673
- ...state,
674
- labelId: label.id(),
675
- }));
676
- }
677
- else if (this.placeholder()) {
678
- this._selectService.state.update((state) => ({
679
- ...state,
680
- labelId: `${state.id}--label`,
681
- }));
682
- }
683
- }
684
589
  ngDoCheck() {
685
590
  this.errorStateTracker.updateErrorState();
686
591
  }
687
592
  toggle() {
688
- if (this.isExpanded()) {
689
- this.close();
593
+ if (this.open()) {
594
+ this.hide();
690
595
  }
691
596
  else {
692
- this.open();
597
+ this.show();
693
598
  }
694
599
  }
695
- open() {
696
- if (!this._canOpen())
600
+ show() {
601
+ if (this.open() || this.disabled() || this._formDisabled() || this.options()?.length == 0) {
697
602
  return;
698
- this._selectService.state.update((state) => ({
699
- ...state,
700
- isExpanded: true,
701
- }));
702
- this.openedChange.emit(true);
703
- this._moveFocusToCDKList();
704
- }
705
- close() {
706
- if (!this.isExpanded())
707
- return;
708
- if (this._selectService.selectTrigger) {
709
- this._selectService.selectTrigger.focus();
710
603
  }
711
- this.openedChange.emit(false);
712
- this._selectService.state.update((state) => ({
713
- ...state,
714
- isExpanded: false,
715
- }));
716
- this._onTouched();
717
- }
718
- _canOpen() {
719
- return !this.isExpanded() && !this.disabled() && this.options()?.length > 0;
604
+ this.open.set(true);
605
+ afterNextRender(() => this.selectContent().focusList(), { injector: this._injector });
720
606
  }
721
- _moveFocusToCDKList() {
722
- setTimeout(() => {
723
- this.selectContent()?.focusList();
724
- });
607
+ hide() {
608
+ if (!this.open())
609
+ return;
610
+ this.open.set(false);
611
+ this._onTouched();
612
+ // restore focus to the trigger
613
+ this.trigger()?.focus();
725
614
  }
726
615
  writeValue(value) {
727
- this.writeValue$.next(value);
616
+ this.value.set(value);
728
617
  }
729
618
  registerOnChange(fn) {
730
619
  this._onChange = fn;
@@ -733,80 +622,65 @@ class BrnSelectComponent {
733
622
  this._onTouched = fn;
734
623
  }
735
624
  setDisabledState(isDisabled) {
736
- this._disabledFromSetDisabledState.set(isDisabled);
625
+ this._formDisabled.set(isDisabled);
737
626
  }
738
- /**
739
- * Once writeValue is called and options are available we can handle setting the initial options
740
- * @private
741
- */
742
- handleInitialOptionSelect() {
743
- // Write value cannot be handled until options are available, so we wait until both are available with a combineLatest
744
- combineLatest([this.writeValue$, this.options$])
745
- .pipe(map((values, index) => [...values, index]), takeUntilDestroyed())
746
- .subscribe(([value, _, index]) => {
747
- this._shouldEmitValueChange.set(false);
748
- this._selectService.setInitialSelectedOptions(value);
749
- // the first time this observable emits a value we are simply setting the initial state
750
- // this change should not count as changing the state of the select, so we need to mark as pristine
751
- if (index === 0) {
752
- this.ngControl?.control?.markAsPristine();
753
- }
754
- });
627
+ selectOption(value) {
628
+ // if this is a multiple select we need to add the value to the array
629
+ if (this.multiple()) {
630
+ const currentValue = this.value();
631
+ const newValue = currentValue ? [...currentValue, value] : [value];
632
+ this.value.set(newValue);
633
+ }
634
+ else {
635
+ this.value.set(value);
636
+ }
637
+ this._onChange?.(this.value());
638
+ // if this is single select close the dropdown
639
+ if (!this.multiple()) {
640
+ this.hide();
641
+ }
755
642
  }
756
- /**
757
- * When options change, our current selected options may become invalid
758
- * Here we will automatically update our current selected options so that they are always inline with the possibleOptions
759
- * @private
760
- */
761
- handleOptionChanges() {
762
- this.optionsAndIndex$.pipe(takeUntilDestroyed()).subscribe(([options, index]) => {
763
- if (index > 0) {
764
- this.handleInvalidOptions(options);
765
- }
766
- this._selectService.updatePossibleOptions(options);
767
- });
643
+ deselectOption(value) {
644
+ if (this.multiple()) {
645
+ const currentValue = this.value();
646
+ const newValue = currentValue.filter((val) => val !== value);
647
+ this.value.set(newValue);
648
+ }
649
+ else {
650
+ this.value.set(null);
651
+ }
652
+ this._onChange?.(this.value());
768
653
  }
769
- /**
770
- * Check that our "selectedOptions" are still valid when "possibleOptions" is about to be updated
771
- */
772
- handleInvalidOptions(options) {
773
- const selectedOptions = this._selectService.selectedOptions();
774
- const availableOptionSet = new Set(options);
775
- if (this._selectService.multiple()) {
776
- const filteredOptions = selectedOptions.filter((o) => availableOptionSet.has(o));
777
- if (selectedOptions.length !== filteredOptions.length) {
778
- const value = filteredOptions.map((o) => o?.value ?? '');
779
- this._selectService.state.update((state) => ({
780
- ...state,
781
- selectedOptions: filteredOptions,
782
- value: value,
783
- }));
784
- }
654
+ toggleSelect(value) {
655
+ if (this.isSelected(value)) {
656
+ this.deselectOption(value);
785
657
  }
786
658
  else {
787
- const selectedOption = selectedOptions[0] ?? null;
788
- if (selectedOption !== null && !availableOptionSet.has(selectedOption)) {
789
- this._selectService.state.update((state) => ({
790
- ...state,
791
- selectedOptions: [],
792
- value: '',
793
- }));
794
- }
659
+ this.selectOption(value);
660
+ }
661
+ }
662
+ isSelected(value) {
663
+ const selection = this.value();
664
+ if (Array.isArray(selection)) {
665
+ return selection.some((val) => this.compareWith()(val, value));
795
666
  }
667
+ else if (value !== undefined) {
668
+ return this.compareWith()(selection, value);
669
+ }
670
+ return false;
796
671
  }
797
672
  /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
798
- /** @nocollapse */ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.5", type: BrnSelectComponent, isStandalone: true, selector: "brn-select, hlm-select", inputs: { multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, dir: { classPropertyName: "dir", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null }, closeDelay: { classPropertyName: "closeDelay", publicName: "closeDelay", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { openedChange: "openedChange", valueChange: "valueChange" }, providers: [
799
- BrnSelectService,
800
- CdkListbox,
673
+ /** @nocollapse */ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.5", type: BrnSelectComponent, isStandalone: true, selector: "brn-select, hlm-select", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, dir: { classPropertyName: "dir", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null }, closeDelay: { classPropertyName: "closeDelay", publicName: "closeDelay", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", value: "valueChange" }, providers: [
801
674
  provideExposedSideProviderExisting((() => BrnSelectComponent)),
802
675
  provideExposesStateProviderExisting((() => BrnSelectComponent)),
676
+ provideBrnSelect(BrnSelectComponent),
803
677
  {
804
678
  provide: BrnFormFieldControl,
805
679
  useExisting: BrnSelectComponent,
806
680
  },
807
- ], queries: [{ propertyName: "selectLabel", first: true, predicate: BrnLabelDirective, isSignal: true }, { propertyName: "selectContent", first: true, predicate: BrnSelectContentComponent, descendants: true, isSignal: true }, { propertyName: "options", predicate: CdkOption, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "_overlayDir", first: true, predicate: CdkConnectedOverlay, descendants: true, isSignal: true }], ngImport: i0, template: `
808
- @if (!labelProvided() && placeholder()) {
809
- <label class="hidden" [attr.id]="backupLabelId()">{{ placeholder() }}</label>
681
+ ], queries: [{ propertyName: "selectLabel", first: true, predicate: BrnLabelDirective, isSignal: true }, { propertyName: "selectContent", first: true, predicate: BrnSelectContentComponent, descendants: true, isSignal: true }, { propertyName: "options", predicate: BrnSelectOptionDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "_overlayDir", first: true, predicate: CdkConnectedOverlay, descendants: true, isSignal: true }], ngImport: i0, template: `
682
+ @if (!selectLabel() && placeholder()) {
683
+ <label class="hidden" [attr.id]="labelId()">{{ placeholder() }}</label>
810
684
  } @else {
811
685
  <ng-content select="label[hlmLabel],label[brnLabel]" />
812
686
  }
@@ -814,6 +688,7 @@ class BrnSelectComponent {
814
688
  <div cdk-overlay-origin (click)="toggle()" #trigger="cdkOverlayOrigin">
815
689
  <ng-content select="hlm-select-trigger,[brnSelectTrigger]" />
816
690
  </div>
691
+
817
692
  <ng-template
818
693
  cdk-connected-overlay
819
694
  cdkConnectedOverlayLockPosition
@@ -823,13 +698,13 @@ class BrnSelectComponent {
823
698
  [cdkConnectedOverlayOpen]="_delayedExpanded()"
824
699
  [cdkConnectedOverlayPositions]="_positions"
825
700
  [cdkConnectedOverlayWidth]="triggerWidth() > 0 ? triggerWidth() : 'auto'"
826
- (backdropClick)="close()"
827
- (detach)="close()"
701
+ (backdropClick)="hide()"
702
+ (detach)="hide()"
828
703
  (positionChange)="_positionChanges$.next($event)"
829
704
  >
830
705
  <ng-content />
831
706
  </ng-template>
832
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: OverlayModule }, { kind: "directive", type: i1$2.CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: i1$2.CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }, { kind: "ngmodule", type: CdkListboxModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
707
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: OverlayModule }, { kind: "directive", type: i1$1.CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: i1$1.CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }, { kind: "ngmodule", type: CdkListboxModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
833
708
  }
834
709
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectComponent, decorators: [{
835
710
  type: Component,
@@ -839,18 +714,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImpor
839
714
  imports: [OverlayModule, CdkListboxModule],
840
715
  changeDetection: ChangeDetectionStrategy.OnPush,
841
716
  providers: [
842
- BrnSelectService,
843
- CdkListbox,
844
717
  provideExposedSideProviderExisting((() => BrnSelectComponent)),
845
718
  provideExposesStateProviderExisting((() => BrnSelectComponent)),
719
+ provideBrnSelect(BrnSelectComponent),
846
720
  {
847
721
  provide: BrnFormFieldControl,
848
722
  useExisting: BrnSelectComponent,
849
723
  },
850
724
  ],
851
725
  template: `
852
- @if (!labelProvided() && placeholder()) {
853
- <label class="hidden" [attr.id]="backupLabelId()">{{ placeholder() }}</label>
726
+ @if (!selectLabel() && placeholder()) {
727
+ <label class="hidden" [attr.id]="labelId()">{{ placeholder() }}</label>
854
728
  } @else {
855
729
  <ng-content select="label[hlmLabel],label[brnLabel]" />
856
730
  }
@@ -858,6 +732,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImpor
858
732
  <div cdk-overlay-origin (click)="toggle()" #trigger="cdkOverlayOrigin">
859
733
  <ng-content select="hlm-select-trigger,[brnSelectTrigger]" />
860
734
  </div>
735
+
861
736
  <ng-template
862
737
  cdk-connected-overlay
863
738
  cdkConnectedOverlayLockPosition
@@ -867,8 +742,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImpor
867
742
  [cdkConnectedOverlayOpen]="_delayedExpanded()"
868
743
  [cdkConnectedOverlayPositions]="_positions"
869
744
  [cdkConnectedOverlayWidth]="triggerWidth() > 0 ? triggerWidth() : 'auto'"
870
- (backdropClick)="close()"
871
- (detach)="close()"
745
+ (backdropClick)="hide()"
746
+ (detach)="hide()"
872
747
  (positionChange)="_positionChanges$.next($event)"
873
748
  >
874
749
  <ng-content />
@@ -887,6 +762,8 @@ const BrnSelectImports = [
887
762
  BrnSelectScrollUpDirective,
888
763
  BrnSelectGroupDirective,
889
764
  BrnSelectLabelDirective,
765
+ BrnSelectValueDirective,
766
+ BrnSelectPlaceholderDirective,
890
767
  ];
891
768
  class BrnSelectModule {
892
769
  /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
@@ -898,7 +775,9 @@ class BrnSelectModule {
898
775
  BrnSelectScrollDownDirective,
899
776
  BrnSelectScrollUpDirective,
900
777
  BrnSelectGroupDirective,
901
- BrnSelectLabelDirective], exports: [BrnSelectComponent,
778
+ BrnSelectLabelDirective,
779
+ BrnSelectValueDirective,
780
+ BrnSelectPlaceholderDirective], exports: [BrnSelectComponent,
902
781
  BrnSelectContentComponent,
903
782
  BrnSelectTriggerDirective,
904
783
  BrnSelectOptionDirective,
@@ -906,7 +785,9 @@ class BrnSelectModule {
906
785
  BrnSelectScrollDownDirective,
907
786
  BrnSelectScrollUpDirective,
908
787
  BrnSelectGroupDirective,
909
- BrnSelectLabelDirective] });
788
+ BrnSelectLabelDirective,
789
+ BrnSelectValueDirective,
790
+ BrnSelectPlaceholderDirective] });
910
791
  /** @nocollapse */ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectModule, imports: [BrnSelectComponent] });
911
792
  }
912
793
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImport: i0, type: BrnSelectModule, decorators: [{
@@ -921,5 +802,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.5", ngImpor
921
802
  * Generated bundle index. Do not edit.
922
803
  */
923
804
 
924
- export { BrnSelectComponent, BrnSelectContentComponent, BrnSelectGroupDirective, BrnSelectImports, BrnSelectLabelDirective, BrnSelectModule, BrnSelectOptionDirective, BrnSelectScrollDownDirective, BrnSelectScrollUpDirective, BrnSelectService, BrnSelectTriggerDirective, BrnSelectValueComponent };
805
+ export { BrnSelectComponent, BrnSelectContentComponent, BrnSelectGroupDirective, BrnSelectImports, BrnSelectLabelDirective, BrnSelectModule, BrnSelectOptionDirective, BrnSelectPlaceholderDirective, BrnSelectScrollDownDirective, BrnSelectScrollUpDirective, BrnSelectTriggerDirective, BrnSelectValueComponent, BrnSelectValueDirective };
925
806
  //# sourceMappingURL=spartan-ng-brain-select.mjs.map