@compsych-ui-components/angular 7.0.26 → 7.0.31

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.
@@ -0,0 +1,438 @@
1
+ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewChild, booleanAttribute, inject, } from '@angular/core';
2
+ import { DomSanitizer } from '@angular/platform-browser';
3
+ import { NgControl, Validators } from '@angular/forms';
4
+ import { CdkListbox, CdkOption } from '@angular/cdk/listbox';
5
+ import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
6
+ import { LUCIDE_ICONS } from '@compsych-ui-components/shared/assets/lucide-icons.generated';
7
+ import * as i0 from "@angular/core";
8
+ let nextId = 0;
9
+ /**
10
+ * A labelled single-select dropdown built on the CDK Listbox (`@angular/cdk/listbox`)
11
+ * and CDK Overlay. The trigger renders the closed field (label, placeholder/selected
12
+ * value and chevron); the options open in an overlay panel with full keyboard support
13
+ * (arrow keys, Home/End, typeahead) provided by the CDK listbox.
14
+ *
15
+ * Implements `ControlValueAccessor` so it works with both reactive forms
16
+ * (`formControlName`, `[formControl]`) and template-driven forms (`ngModel`). The error
17
+ * visual state is driven automatically by the bound `FormControl`'s validity once the
18
+ * field has been touched, and can also be set explicitly via the `error` input.
19
+ */
20
+ export class DropdownComponent {
21
+ _autoId = `cmp-dropdown-${++nextId}`;
22
+ cdr = inject(ChangeDetectorRef);
23
+ sanitizer = inject(DomSanitizer);
24
+ _onChange = () => { };
25
+ _onTouched = () => { };
26
+ /** Injected when the component is used inside a reactive or template-driven form. */
27
+ ngControl;
28
+ constructor() {
29
+ const ctrl = inject(NgControl, { optional: true, self: true });
30
+ if (ctrl)
31
+ ctrl.valueAccessor = this;
32
+ this.ngControl = ctrl;
33
+ this.chevronSvg = this.buildIconSvg('chevron-down');
34
+ this.checkSvg = this.buildIconSvg('check');
35
+ }
36
+ triggerBtn;
37
+ listboxEl;
38
+ /** Label displayed above the trigger. */
39
+ label = '';
40
+ /** Text shown in the trigger when no option is selected. */
41
+ placeholder = '';
42
+ /** Currently selected value. When used in a form, the form controls the value. */
43
+ value = '';
44
+ /** Selectable options rendered in the overlay panel. */
45
+ options = [];
46
+ /** Size variant — controls height, padding, and font size. Defaults to `'medium'`. */
47
+ size = 'medium';
48
+ /** Native `id` attribute for the trigger. Auto-generated if omitted. */
49
+ id;
50
+ /**
51
+ * Id(s) of external elements that describe the field, space-separated.
52
+ * Forwarded to `aria-describedby` on the trigger.
53
+ */
54
+ ariaDescribedBy;
55
+ /** Text displayed below the trigger. Shown in red when in error state. */
56
+ supportingText = '';
57
+ /**
58
+ * Static error text shown in red when in error state. Used when the dropdown is
59
+ * outside a form (with `[error]="true"`). When inside a form, prefer `errorMessages`.
60
+ */
61
+ errorMessage = '';
62
+ /**
63
+ * Map of Angular validator error keys to display strings. The component reads
64
+ * `ngControl.errors` to determine which validator failed and shows the matching
65
+ * message in red below the trigger. Example: `{ required: 'Please choose an option' }`
66
+ */
67
+ errorMessages = {};
68
+ /**
69
+ * Adds a blue asterisk after the label. When used in a form, this is set
70
+ * automatically if the bound `FormControl` has `Validators.required`.
71
+ * Supports boolean coercion.
72
+ */
73
+ required = false;
74
+ /**
75
+ * Applies the error visual state (red border) explicitly. When used in a form,
76
+ * the error state is driven automatically from the `FormControl`'s validity once
77
+ * the field is touched — this input is only needed outside of forms. Supports boolean coercion.
78
+ */
79
+ error = false;
80
+ /** Disables the dropdown. When used in a form, also responds to `FormControl.disable()`. Supports boolean coercion. */
81
+ disabled = false;
82
+ /** Visually hides the label while keeping it accessible to screen readers. Supports boolean coercion. */
83
+ hideLabel = false;
84
+ /** Emits the newly selected value whenever the user picks an option. */
85
+ valueChange = new EventEmitter();
86
+ /** Emitted when the overlay panel opens. */
87
+ opened = new EventEmitter();
88
+ /** Emitted when the overlay panel closes. */
89
+ closed = new EventEmitter();
90
+ chevronSvg;
91
+ checkSvg;
92
+ isOpen = false;
93
+ overlayWidth = 0;
94
+ overlayPositions = [
95
+ { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
96
+ { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },
97
+ ];
98
+ buildIconSvg(name) {
99
+ const svg = (LUCIDE_ICONS[name] ?? '').replace('<svg ', '<svg width="20" height="20" ');
100
+ return this.sanitizer.bypassSecurityTrustHtml(svg);
101
+ }
102
+ get triggerId() {
103
+ return this.id ?? this._autoId;
104
+ }
105
+ get labelId() {
106
+ return this.triggerId + '-label';
107
+ }
108
+ get listboxId() {
109
+ return this.triggerId + '-listbox';
110
+ }
111
+ get selectedOption() {
112
+ return this.options.find((o) => o.value === this.value);
113
+ }
114
+ /** Value array bound to the CDK listbox (single-select, so 0 or 1 entry). */
115
+ get listboxValue() {
116
+ return this.value ? [this.value] : [];
117
+ }
118
+ /** True when the bound FormControl is invalid and touched, or when `error` is explicitly set. */
119
+ get hasError() {
120
+ return this.error || !!(this.ngControl?.invalid && this.ngControl.touched);
121
+ }
122
+ /** True when `required` is set explicitly, or when the bound FormControl has Validators.required. */
123
+ get isRequired() {
124
+ return this.required || !!this.ngControl?.control?.hasValidator(Validators.required);
125
+ }
126
+ get activeErrorMessage() {
127
+ const errors = this.ngControl?.errors;
128
+ if (!errors)
129
+ return '';
130
+ for (const key of Object.keys(errors)) {
131
+ if (this.errorMessages[key])
132
+ return this.errorMessages[key];
133
+ }
134
+ return '';
135
+ }
136
+ get displayedSupportingText() {
137
+ if (this.hasError) {
138
+ return this.activeErrorMessage || this.errorMessage || this.supportingText;
139
+ }
140
+ return this.supportingText;
141
+ }
142
+ get describedBy() {
143
+ const parts = [];
144
+ if (this.ariaDescribedBy)
145
+ parts.push(this.ariaDescribedBy);
146
+ if (this.supportingText || this.errorMessage || Object.keys(this.errorMessages).length) {
147
+ parts.push(this.triggerId + '-supporting');
148
+ }
149
+ return parts.length ? parts.join(' ') : null;
150
+ }
151
+ get wrapperClasses() {
152
+ return ['cmp-dropdown', this.size, this.disabled ? 'disabled' : '']
153
+ .filter(Boolean)
154
+ .join(' ');
155
+ }
156
+ // ── Open / close ──────────────────────────────────────────────────────────
157
+ toggle() {
158
+ if (this.isOpen) {
159
+ this.close(true);
160
+ }
161
+ else {
162
+ this.open();
163
+ }
164
+ }
165
+ open() {
166
+ if (this.disabled || this.isOpen)
167
+ return;
168
+ this.overlayWidth = this.triggerBtn?.nativeElement.offsetWidth ?? 0;
169
+ this.isOpen = true;
170
+ this.opened.emit();
171
+ this.cdr.markForCheck();
172
+ }
173
+ close(refocus = false) {
174
+ if (!this.isOpen)
175
+ return;
176
+ this.isOpen = false;
177
+ this._onTouched();
178
+ this.closed.emit();
179
+ if (refocus) {
180
+ setTimeout(() => this.triggerBtn?.nativeElement.focus());
181
+ }
182
+ this.cdr.markForCheck();
183
+ }
184
+ onPanelAttach() {
185
+ setTimeout(() => this.listboxEl?.nativeElement.focus());
186
+ }
187
+ // ── Keyboard ──────────────────────────────────────────────────────────────
188
+ onTriggerKeydown(event) {
189
+ if (this.disabled)
190
+ return;
191
+ if ((event.key === 'ArrowDown' || event.key === 'ArrowUp') && !this.isOpen) {
192
+ event.preventDefault();
193
+ this.open();
194
+ }
195
+ else if (event.key === 'Escape' && this.isOpen) {
196
+ event.preventDefault();
197
+ this.close(true);
198
+ }
199
+ }
200
+ onListboxKeydown(event) {
201
+ if (event.key === 'Escape') {
202
+ event.preventDefault();
203
+ this.close(true);
204
+ }
205
+ else if (event.key === 'Tab') {
206
+ event.preventDefault();
207
+ this.close(true);
208
+ }
209
+ }
210
+ onListboxValueChange(event) {
211
+ const next = event.value[0] ?? '';
212
+ this.value = next;
213
+ this._onChange(next);
214
+ this.valueChange.emit(next);
215
+ this.close(true);
216
+ }
217
+ // ── ControlValueAccessor ────────────────────────────────────────────────────
218
+ writeValue(value) {
219
+ this.value = value ?? '';
220
+ this.cdr.markForCheck();
221
+ }
222
+ registerOnChange(fn) {
223
+ this._onChange = fn;
224
+ }
225
+ registerOnTouched(fn) {
226
+ this._onTouched = fn;
227
+ }
228
+ /** Called by Angular when the bound FormControl is programmatically disabled or enabled. */
229
+ setDisabledState(isDisabled) {
230
+ this.disabled = isDisabled;
231
+ this.cdr.markForCheck();
232
+ }
233
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DropdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
234
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: DropdownComponent, isStandalone: true, selector: "compsych-dropdown", inputs: { label: "label", placeholder: "placeholder", value: "value", options: "options", size: "size", id: "id", ariaDescribedBy: "ariaDescribedBy", supportingText: "supportingText", errorMessage: "errorMessage", errorMessages: "errorMessages", required: ["required", "required", booleanAttribute], error: ["error", "error", booleanAttribute], disabled: ["disabled", "disabled", booleanAttribute], hideLabel: ["hideLabel", "hideLabel", booleanAttribute] }, outputs: { valueChange: "valueChange", opened: "opened", closed: "closed" }, viewQueries: [{ propertyName: "triggerBtn", first: true, predicate: ["triggerBtn"], descendants: true }, { propertyName: "listboxEl", first: true, predicate: ["listboxEl"], descendants: true }], ngImport: i0, template: `
235
+ <div [class]="wrapperClasses">
236
+ <div class="label-row" [class.sr-only]="hideLabel">
237
+ <label class="label" [id]="labelId" [for]="triggerId">
238
+ {{ label }}
239
+ @if (isRequired) {
240
+ <span class="required-marker" aria-hidden="true"> *</span>
241
+ }
242
+ </label>
243
+ </div>
244
+
245
+ <button
246
+ #triggerBtn
247
+ cdkOverlayOrigin
248
+ #origin="cdkOverlayOrigin"
249
+ type="button"
250
+ class="trigger"
251
+ [id]="triggerId"
252
+ [class.placeholder]="!selectedOption"
253
+ [class.error]="hasError"
254
+ [class.open]="isOpen"
255
+ [disabled]="disabled"
256
+ role="combobox"
257
+ aria-haspopup="listbox"
258
+ [attr.aria-expanded]="isOpen"
259
+ [attr.aria-controls]="listboxId"
260
+ [attr.aria-labelledby]="labelId + ' ' + triggerId"
261
+ [attr.aria-invalid]="hasError ? 'true' : null"
262
+ [attr.aria-required]="isRequired ? 'true' : null"
263
+ [attr.aria-describedby]="describedBy"
264
+ (click)="toggle()"
265
+ (keydown)="onTriggerKeydown($event)"
266
+ >
267
+ <span class="trigger-text">{{ selectedOption?.label || placeholder }}</span>
268
+ <span class="trigger-chevron" aria-hidden="true" [innerHTML]="chevronSvg"></span>
269
+ </button>
270
+
271
+ @if (displayedSupportingText) {
272
+ <span class="supporting-text" [class.supporting-text--error]="hasError" [id]="triggerId + '-supporting'">
273
+ {{ displayedSupportingText }}
274
+ </span>
275
+ }
276
+ </div>
277
+
278
+ <ng-template
279
+ cdkConnectedOverlay
280
+ [cdkConnectedOverlayOrigin]="origin"
281
+ [cdkConnectedOverlayOpen]="isOpen"
282
+ [cdkConnectedOverlayWidth]="overlayWidth"
283
+ [cdkConnectedOverlayPositions]="overlayPositions"
284
+ [cdkConnectedOverlayViewportMargin]="8"
285
+ (overlayOutsideClick)="close()"
286
+ (attach)="onPanelAttach()"
287
+ (detach)="close()"
288
+ >
289
+ <ul
290
+ #listboxEl
291
+ cdkListbox
292
+ class="panel"
293
+ [id]="listboxId"
294
+ [attr.aria-labelledby]="labelId"
295
+ [cdkListboxValue]="listboxValue"
296
+ (cdkListboxValueChange)="onListboxValueChange($event)"
297
+ (keydown)="onListboxKeydown($event)"
298
+ >
299
+ @for (opt of options; track opt.value) {
300
+ <li class="option" [cdkOption]="opt.value" [cdkOptionDisabled]="!!opt.disabled">
301
+ <span class="option-check" aria-hidden="true">
302
+ @if (opt.value === value) {
303
+ <span [innerHTML]="checkSvg"></span>
304
+ }
305
+ </span>
306
+ <span class="option-label">{{ opt.label }}</span>
307
+ </li>
308
+ }
309
+ </ul>
310
+ </ng-template>
311
+ `, isInline: true, styles: [":host{display:block}.cmp-dropdown{display:flex;flex-direction:column;gap:var(--spacing-padding-sys-padding-8)}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.label-row{display:flex;justify-content:space-between;align-items:center;gap:var(--spacing-padding-sys-padding-8)}.label{font-family:var(--label-medium-sys-font-family);font-size:var(--label-medium-sys-font-size);font-weight:var(--label-medium-sys-font-weight-emphasized);line-height:var(--label-medium-sys-line-height);letter-spacing:var(--label-medium-sys-tracking);color:var(--surface-surface-sys-on-surface)}.cmp-dropdown.large .label{font-family:var(--label-large-sys-font-family);font-size:var(--label-large-sys-font-size);font-weight:var(--label-large-sys-font-weight-emphasized);line-height:var(--label-large-sys-line-height);letter-spacing:var(--label-large-sys-tracking)}.required-marker{color:var(--custom-info-sys-info)}.cmp-dropdown.disabled .label{opacity:.48}.trigger{box-sizing:border-box;width:100%;display:flex;align-items:center;gap:var(--spacing-padding-sys-padding-8);margin:0;border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline-variant);border-radius:var(--border-radius-sys-radius-sm);background-color:var(--surface-surface-container-sys-surface-container-lowest);color:var(--surface-surface-sys-on-surface);font-family:var(--body-medium-sys-font-family);font-weight:var(--body-medium-sys-font-weight);text-align:start;cursor:pointer;appearance:none;-webkit-appearance:none;outline:none;transition:border-color .15s ease,box-shadow .15s ease,background-color .15s ease}.trigger-text{flex:1 0 0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.trigger.placeholder .trigger-text{color:var(--surface-surface-sys-on-surface-variant)}.trigger-chevron{display:flex;align-items:center;flex-shrink:0;color:var(--surface-surface-sys-on-surface-variant);transition:transform .15s ease}.trigger.open .trigger-chevron{transform:rotate(180deg)}.trigger:hover:not(:disabled):not(.open){box-shadow:0 4px 16px var(--transparent-neutral-sys-black-10),0 2px 4px #00000014}.trigger:focus-visible,.trigger.open{border-color:var(--accent-primary-sys-primary);border-width:var(--border-width-sys-stroke-medium);box-shadow:0 0 0 4px var(--transparent-primary-sys-primary-08)}.trigger.error:not(:disabled):not(.open){border-color:var(--error-sys-error)}.trigger.error:focus-visible,.trigger.error.open{border-color:var(--error-sys-error);border-width:var(--border-width-sys-stroke-medium);box-shadow:0 0 0 4px var(--transparent-primary-sys-primary-08)}.trigger:disabled{background-color:var(--surface-surface-container-sys-surface-container-low);color:var(--surface-surface-sys-on-surface-variant);border-color:var(--outline-sys-outline-variant);cursor:not-allowed;opacity:.48}.supporting-text{font-family:var(--body-small-sys-font-family);font-size:var(--body-small-sys-font-size);font-weight:var(--body-small-sys-font-weight);line-height:var(--body-small-sys-line-height);color:var(--surface-surface-sys-on-surface-variant)}.supporting-text--error{color:var(--error-sys-error)}.cmp-dropdown.disabled .supporting-text{opacity:.48}.cmp-dropdown.small .trigger{height:40px;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-12);font-size:var(--body-small-sys-font-size);line-height:var(--body-small-sys-line-height)}.cmp-dropdown.medium .trigger{height:48px;padding:var(--spacing-padding-sys-padding-12) var(--spacing-padding-sys-padding-16);font-size:var(--body-medium-sys-font-size);line-height:var(--body-medium-sys-line-height)}.cmp-dropdown.large .trigger{height:56px;padding:var(--spacing-padding-sys-padding-16) var(--spacing-padding-sys-padding-20);border-radius:var(--border-radius-sys-radius-md);font-size:var(--body-medium-sys-font-size);line-height:var(--body-medium-sys-line-height)}.panel{box-sizing:border-box;list-style:none;margin:0;padding:var(--spacing-padding-sys-padding-4);max-height:280px;overflow-y:auto;background-color:var(--surface-surface-container-sys-surface-container-lowest);border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline-variant);border-radius:var(--border-radius-sys-radius-sm);box-shadow:0 4px 16px var(--transparent-neutral-sys-black-10),0 2px 4px #00000014;outline:none;font-family:var(--body-medium-sys-font-family)}.option{box-sizing:border-box;display:flex;align-items:center;gap:var(--spacing-padding-sys-padding-8);padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-12);border-radius:var(--border-radius-sys-radius-sm);color:var(--surface-surface-sys-on-surface);font-size:var(--body-medium-sys-font-size);line-height:var(--body-medium-sys-line-height);cursor:pointer;outline:none;-webkit-user-select:none;user-select:none}.option-check{display:flex;align-items:center;justify-content:center;width:20px;height:20px;flex-shrink:0;color:var(--accent-primary-sys-primary)}.option-label{flex:1 0 0;min-width:0}.option:hover:not([aria-disabled=true]){background-color:var(--transparent-primary-sys-primary-08)}.option.cdk-option-active{background-color:var(--transparent-primary-sys-primary-08);outline:var(--border-width-sys-stroke-medium) solid var(--accent-primary-sys-primary);outline-offset:-2px}.option[aria-selected=true]{color:var(--accent-primary-sys-primary)}.option[aria-disabled=true]{color:var(--surface-surface-sys-on-surface-variant);cursor:not-allowed;opacity:.48}@media(forced-colors:active){.trigger{border:1px solid ButtonText;forced-color-adjust:none}.trigger:focus-visible,.trigger.open{outline:2px solid Highlight;outline-offset:2px}.trigger:disabled{color:GrayText;border-color:GrayText;background:ButtonFace;opacity:1}.cmp-dropdown.disabled .label{color:GrayText;opacity:1}.supporting-text--error{color:LinkText}.panel{border:1px solid ButtonText}.option.cdk-option-active{outline:2px solid Highlight;outline-offset:-2px}}\n"], dependencies: [{ kind: "directive", type: CdkListbox, selector: "[cdkListbox]", inputs: ["id", "tabindex", "cdkListboxValue", "cdkListboxMultiple", "cdkListboxDisabled", "cdkListboxUseActiveDescendant", "cdkListboxOrientation", "cdkListboxCompareWith", "cdkListboxNavigationWrapDisabled", "cdkListboxNavigatesDisabledOptions"], outputs: ["cdkListboxValueChange"], exportAs: ["cdkListbox"] }, { kind: "directive", type: CdkOption, selector: "[cdkOption]", inputs: ["id", "cdkOption", "cdkOptionTypeaheadLabel", "cdkOptionDisabled", "tabindex"], exportAs: ["cdkOption"] }, { kind: "directive", type: 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", "cdkConnectedOverlayUsePopover", "cdkConnectedOverlayMatchWidth", "cdkConnectedOverlay"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
312
+ }
313
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DropdownComponent, decorators: [{
314
+ type: Component,
315
+ args: [{ selector: 'compsych-dropdown', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CdkListbox, CdkOption, CdkConnectedOverlay, CdkOverlayOrigin], template: `
316
+ <div [class]="wrapperClasses">
317
+ <div class="label-row" [class.sr-only]="hideLabel">
318
+ <label class="label" [id]="labelId" [for]="triggerId">
319
+ {{ label }}
320
+ @if (isRequired) {
321
+ <span class="required-marker" aria-hidden="true"> *</span>
322
+ }
323
+ </label>
324
+ </div>
325
+
326
+ <button
327
+ #triggerBtn
328
+ cdkOverlayOrigin
329
+ #origin="cdkOverlayOrigin"
330
+ type="button"
331
+ class="trigger"
332
+ [id]="triggerId"
333
+ [class.placeholder]="!selectedOption"
334
+ [class.error]="hasError"
335
+ [class.open]="isOpen"
336
+ [disabled]="disabled"
337
+ role="combobox"
338
+ aria-haspopup="listbox"
339
+ [attr.aria-expanded]="isOpen"
340
+ [attr.aria-controls]="listboxId"
341
+ [attr.aria-labelledby]="labelId + ' ' + triggerId"
342
+ [attr.aria-invalid]="hasError ? 'true' : null"
343
+ [attr.aria-required]="isRequired ? 'true' : null"
344
+ [attr.aria-describedby]="describedBy"
345
+ (click)="toggle()"
346
+ (keydown)="onTriggerKeydown($event)"
347
+ >
348
+ <span class="trigger-text">{{ selectedOption?.label || placeholder }}</span>
349
+ <span class="trigger-chevron" aria-hidden="true" [innerHTML]="chevronSvg"></span>
350
+ </button>
351
+
352
+ @if (displayedSupportingText) {
353
+ <span class="supporting-text" [class.supporting-text--error]="hasError" [id]="triggerId + '-supporting'">
354
+ {{ displayedSupportingText }}
355
+ </span>
356
+ }
357
+ </div>
358
+
359
+ <ng-template
360
+ cdkConnectedOverlay
361
+ [cdkConnectedOverlayOrigin]="origin"
362
+ [cdkConnectedOverlayOpen]="isOpen"
363
+ [cdkConnectedOverlayWidth]="overlayWidth"
364
+ [cdkConnectedOverlayPositions]="overlayPositions"
365
+ [cdkConnectedOverlayViewportMargin]="8"
366
+ (overlayOutsideClick)="close()"
367
+ (attach)="onPanelAttach()"
368
+ (detach)="close()"
369
+ >
370
+ <ul
371
+ #listboxEl
372
+ cdkListbox
373
+ class="panel"
374
+ [id]="listboxId"
375
+ [attr.aria-labelledby]="labelId"
376
+ [cdkListboxValue]="listboxValue"
377
+ (cdkListboxValueChange)="onListboxValueChange($event)"
378
+ (keydown)="onListboxKeydown($event)"
379
+ >
380
+ @for (opt of options; track opt.value) {
381
+ <li class="option" [cdkOption]="opt.value" [cdkOptionDisabled]="!!opt.disabled">
382
+ <span class="option-check" aria-hidden="true">
383
+ @if (opt.value === value) {
384
+ <span [innerHTML]="checkSvg"></span>
385
+ }
386
+ </span>
387
+ <span class="option-label">{{ opt.label }}</span>
388
+ </li>
389
+ }
390
+ </ul>
391
+ </ng-template>
392
+ `, styles: [":host{display:block}.cmp-dropdown{display:flex;flex-direction:column;gap:var(--spacing-padding-sys-padding-8)}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.label-row{display:flex;justify-content:space-between;align-items:center;gap:var(--spacing-padding-sys-padding-8)}.label{font-family:var(--label-medium-sys-font-family);font-size:var(--label-medium-sys-font-size);font-weight:var(--label-medium-sys-font-weight-emphasized);line-height:var(--label-medium-sys-line-height);letter-spacing:var(--label-medium-sys-tracking);color:var(--surface-surface-sys-on-surface)}.cmp-dropdown.large .label{font-family:var(--label-large-sys-font-family);font-size:var(--label-large-sys-font-size);font-weight:var(--label-large-sys-font-weight-emphasized);line-height:var(--label-large-sys-line-height);letter-spacing:var(--label-large-sys-tracking)}.required-marker{color:var(--custom-info-sys-info)}.cmp-dropdown.disabled .label{opacity:.48}.trigger{box-sizing:border-box;width:100%;display:flex;align-items:center;gap:var(--spacing-padding-sys-padding-8);margin:0;border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline-variant);border-radius:var(--border-radius-sys-radius-sm);background-color:var(--surface-surface-container-sys-surface-container-lowest);color:var(--surface-surface-sys-on-surface);font-family:var(--body-medium-sys-font-family);font-weight:var(--body-medium-sys-font-weight);text-align:start;cursor:pointer;appearance:none;-webkit-appearance:none;outline:none;transition:border-color .15s ease,box-shadow .15s ease,background-color .15s ease}.trigger-text{flex:1 0 0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.trigger.placeholder .trigger-text{color:var(--surface-surface-sys-on-surface-variant)}.trigger-chevron{display:flex;align-items:center;flex-shrink:0;color:var(--surface-surface-sys-on-surface-variant);transition:transform .15s ease}.trigger.open .trigger-chevron{transform:rotate(180deg)}.trigger:hover:not(:disabled):not(.open){box-shadow:0 4px 16px var(--transparent-neutral-sys-black-10),0 2px 4px #00000014}.trigger:focus-visible,.trigger.open{border-color:var(--accent-primary-sys-primary);border-width:var(--border-width-sys-stroke-medium);box-shadow:0 0 0 4px var(--transparent-primary-sys-primary-08)}.trigger.error:not(:disabled):not(.open){border-color:var(--error-sys-error)}.trigger.error:focus-visible,.trigger.error.open{border-color:var(--error-sys-error);border-width:var(--border-width-sys-stroke-medium);box-shadow:0 0 0 4px var(--transparent-primary-sys-primary-08)}.trigger:disabled{background-color:var(--surface-surface-container-sys-surface-container-low);color:var(--surface-surface-sys-on-surface-variant);border-color:var(--outline-sys-outline-variant);cursor:not-allowed;opacity:.48}.supporting-text{font-family:var(--body-small-sys-font-family);font-size:var(--body-small-sys-font-size);font-weight:var(--body-small-sys-font-weight);line-height:var(--body-small-sys-line-height);color:var(--surface-surface-sys-on-surface-variant)}.supporting-text--error{color:var(--error-sys-error)}.cmp-dropdown.disabled .supporting-text{opacity:.48}.cmp-dropdown.small .trigger{height:40px;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-12);font-size:var(--body-small-sys-font-size);line-height:var(--body-small-sys-line-height)}.cmp-dropdown.medium .trigger{height:48px;padding:var(--spacing-padding-sys-padding-12) var(--spacing-padding-sys-padding-16);font-size:var(--body-medium-sys-font-size);line-height:var(--body-medium-sys-line-height)}.cmp-dropdown.large .trigger{height:56px;padding:var(--spacing-padding-sys-padding-16) var(--spacing-padding-sys-padding-20);border-radius:var(--border-radius-sys-radius-md);font-size:var(--body-medium-sys-font-size);line-height:var(--body-medium-sys-line-height)}.panel{box-sizing:border-box;list-style:none;margin:0;padding:var(--spacing-padding-sys-padding-4);max-height:280px;overflow-y:auto;background-color:var(--surface-surface-container-sys-surface-container-lowest);border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline-variant);border-radius:var(--border-radius-sys-radius-sm);box-shadow:0 4px 16px var(--transparent-neutral-sys-black-10),0 2px 4px #00000014;outline:none;font-family:var(--body-medium-sys-font-family)}.option{box-sizing:border-box;display:flex;align-items:center;gap:var(--spacing-padding-sys-padding-8);padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-12);border-radius:var(--border-radius-sys-radius-sm);color:var(--surface-surface-sys-on-surface);font-size:var(--body-medium-sys-font-size);line-height:var(--body-medium-sys-line-height);cursor:pointer;outline:none;-webkit-user-select:none;user-select:none}.option-check{display:flex;align-items:center;justify-content:center;width:20px;height:20px;flex-shrink:0;color:var(--accent-primary-sys-primary)}.option-label{flex:1 0 0;min-width:0}.option:hover:not([aria-disabled=true]){background-color:var(--transparent-primary-sys-primary-08)}.option.cdk-option-active{background-color:var(--transparent-primary-sys-primary-08);outline:var(--border-width-sys-stroke-medium) solid var(--accent-primary-sys-primary);outline-offset:-2px}.option[aria-selected=true]{color:var(--accent-primary-sys-primary)}.option[aria-disabled=true]{color:var(--surface-surface-sys-on-surface-variant);cursor:not-allowed;opacity:.48}@media(forced-colors:active){.trigger{border:1px solid ButtonText;forced-color-adjust:none}.trigger:focus-visible,.trigger.open{outline:2px solid Highlight;outline-offset:2px}.trigger:disabled{color:GrayText;border-color:GrayText;background:ButtonFace;opacity:1}.cmp-dropdown.disabled .label{color:GrayText;opacity:1}.supporting-text--error{color:LinkText}.panel{border:1px solid ButtonText}.option.cdk-option-active{outline:2px solid Highlight;outline-offset:-2px}}\n"] }]
393
+ }], ctorParameters: () => [], propDecorators: { triggerBtn: [{
394
+ type: ViewChild,
395
+ args: ['triggerBtn']
396
+ }], listboxEl: [{
397
+ type: ViewChild,
398
+ args: ['listboxEl']
399
+ }], label: [{
400
+ type: Input
401
+ }], placeholder: [{
402
+ type: Input
403
+ }], value: [{
404
+ type: Input
405
+ }], options: [{
406
+ type: Input
407
+ }], size: [{
408
+ type: Input
409
+ }], id: [{
410
+ type: Input
411
+ }], ariaDescribedBy: [{
412
+ type: Input
413
+ }], supportingText: [{
414
+ type: Input
415
+ }], errorMessage: [{
416
+ type: Input
417
+ }], errorMessages: [{
418
+ type: Input
419
+ }], required: [{
420
+ type: Input,
421
+ args: [{ transform: booleanAttribute }]
422
+ }], error: [{
423
+ type: Input,
424
+ args: [{ transform: booleanAttribute }]
425
+ }], disabled: [{
426
+ type: Input,
427
+ args: [{ transform: booleanAttribute }]
428
+ }], hideLabel: [{
429
+ type: Input,
430
+ args: [{ transform: booleanAttribute }]
431
+ }], valueChange: [{
432
+ type: Output
433
+ }], opened: [{
434
+ type: Output
435
+ }], closed: [{
436
+ type: Output
437
+ }] } });
438
+ //# sourceMappingURL=dropdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dropdown.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/dropdown/dropdown.ts"],"names":[],"mappings":"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","sourcesContent":["import {\r\n ChangeDetectionStrategy,\r\n ChangeDetectorRef,\r\n Component,\r\n ElementRef,\r\n EventEmitter,\r\n Input,\r\n Output,\r\n ViewChild,\r\n booleanAttribute,\r\n inject,\r\n} from '@angular/core';\r\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\r\nimport { ControlValueAccessor, NgControl, Validators } from '@angular/forms';\r\nimport { CdkListbox, CdkOption, ListboxValueChangeEvent } from '@angular/cdk/listbox';\r\nimport { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedPosition } from '@angular/cdk/overlay';\r\nimport { LUCIDE_ICONS } from '@compsych-ui-components/shared/assets/lucide-icons.generated';\r\nimport { DropdownOption, DropdownSize } from './dropdown-types';\r\n\r\nlet nextId = 0;\r\n\r\n/**\r\n * A labelled single-select dropdown built on the CDK Listbox (`@angular/cdk/listbox`)\r\n * and CDK Overlay. The trigger renders the closed field (label, placeholder/selected\r\n * value and chevron); the options open in an overlay panel with full keyboard support\r\n * (arrow keys, Home/End, typeahead) provided by the CDK listbox.\r\n *\r\n * Implements `ControlValueAccessor` so it works with both reactive forms\r\n * (`formControlName`, `[formControl]`) and template-driven forms (`ngModel`). The error\r\n * visual state is driven automatically by the bound `FormControl`'s validity once the\r\n * field has been touched, and can also be set explicitly via the `error` input.\r\n */\r\n@Component({\r\n selector: 'compsych-dropdown',\r\n standalone: true,\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n imports: [CdkListbox, CdkOption, CdkConnectedOverlay, CdkOverlayOrigin],\r\n template: `\r\n <div [class]=\"wrapperClasses\">\r\n <div class=\"label-row\" [class.sr-only]=\"hideLabel\">\r\n <label class=\"label\" [id]=\"labelId\" [for]=\"triggerId\">\r\n {{ label }}\r\n @if (isRequired) {\r\n <span class=\"required-marker\" aria-hidden=\"true\"> *</span>\r\n }\r\n </label>\r\n </div>\r\n\r\n <button\r\n #triggerBtn\r\n cdkOverlayOrigin\r\n #origin=\"cdkOverlayOrigin\"\r\n type=\"button\"\r\n class=\"trigger\"\r\n [id]=\"triggerId\"\r\n [class.placeholder]=\"!selectedOption\"\r\n [class.error]=\"hasError\"\r\n [class.open]=\"isOpen\"\r\n [disabled]=\"disabled\"\r\n role=\"combobox\"\r\n aria-haspopup=\"listbox\"\r\n [attr.aria-expanded]=\"isOpen\"\r\n [attr.aria-controls]=\"listboxId\"\r\n [attr.aria-labelledby]=\"labelId + ' ' + triggerId\"\r\n [attr.aria-invalid]=\"hasError ? 'true' : null\"\r\n [attr.aria-required]=\"isRequired ? 'true' : null\"\r\n [attr.aria-describedby]=\"describedBy\"\r\n (click)=\"toggle()\"\r\n (keydown)=\"onTriggerKeydown($event)\"\r\n >\r\n <span class=\"trigger-text\">{{ selectedOption?.label || placeholder }}</span>\r\n <span class=\"trigger-chevron\" aria-hidden=\"true\" [innerHTML]=\"chevronSvg\"></span>\r\n </button>\r\n\r\n @if (displayedSupportingText) {\r\n <span class=\"supporting-text\" [class.supporting-text--error]=\"hasError\" [id]=\"triggerId + '-supporting'\">\r\n {{ displayedSupportingText }}\r\n </span>\r\n }\r\n </div>\r\n\r\n <ng-template\r\n cdkConnectedOverlay\r\n [cdkConnectedOverlayOrigin]=\"origin\"\r\n [cdkConnectedOverlayOpen]=\"isOpen\"\r\n [cdkConnectedOverlayWidth]=\"overlayWidth\"\r\n [cdkConnectedOverlayPositions]=\"overlayPositions\"\r\n [cdkConnectedOverlayViewportMargin]=\"8\"\r\n (overlayOutsideClick)=\"close()\"\r\n (attach)=\"onPanelAttach()\"\r\n (detach)=\"close()\"\r\n >\r\n <ul\r\n #listboxEl\r\n cdkListbox\r\n class=\"panel\"\r\n [id]=\"listboxId\"\r\n [attr.aria-labelledby]=\"labelId\"\r\n [cdkListboxValue]=\"listboxValue\"\r\n (cdkListboxValueChange)=\"onListboxValueChange($event)\"\r\n (keydown)=\"onListboxKeydown($event)\"\r\n >\r\n @for (opt of options; track opt.value) {\r\n <li class=\"option\" [cdkOption]=\"opt.value\" [cdkOptionDisabled]=\"!!opt.disabled\">\r\n <span class=\"option-check\" aria-hidden=\"true\">\r\n @if (opt.value === value) {\r\n <span [innerHTML]=\"checkSvg\"></span>\r\n }\r\n </span>\r\n <span class=\"option-label\">{{ opt.label }}</span>\r\n </li>\r\n }\r\n </ul>\r\n </ng-template>\r\n `,\r\n styleUrl: './dropdown.css',\r\n})\r\nexport class DropdownComponent implements ControlValueAccessor {\r\n private readonly _autoId = `cmp-dropdown-${++nextId}`;\r\n private readonly cdr = inject(ChangeDetectorRef);\r\n private readonly sanitizer = inject(DomSanitizer);\r\n private _onChange: (value: string) => void = () => {};\r\n private _onTouched: () => void = () => {};\r\n\r\n /** Injected when the component is used inside a reactive or template-driven form. */\r\n protected readonly ngControl: NgControl | null;\r\n\r\n constructor() {\r\n const ctrl = inject(NgControl, { optional: true, self: true });\r\n if (ctrl) ctrl.valueAccessor = this;\r\n this.ngControl = ctrl;\r\n this.chevronSvg = this.buildIconSvg('chevron-down');\r\n this.checkSvg = this.buildIconSvg('check');\r\n }\r\n\r\n @ViewChild('triggerBtn') private triggerBtn?: ElementRef<HTMLButtonElement>;\r\n @ViewChild('listboxEl') private listboxEl?: ElementRef<HTMLUListElement>;\r\n\r\n /** Label displayed above the trigger. */\r\n @Input() label = '';\r\n /** Text shown in the trigger when no option is selected. */\r\n @Input() placeholder = '';\r\n /** Currently selected value. When used in a form, the form controls the value. */\r\n @Input() value = '';\r\n /** Selectable options rendered in the overlay panel. */\r\n @Input() options: DropdownOption[] = [];\r\n /** Size variant — controls height, padding, and font size. Defaults to `'medium'`. */\r\n @Input() size: DropdownSize = 'medium';\r\n /** Native `id` attribute for the trigger. Auto-generated if omitted. */\r\n @Input() id?: string;\r\n /**\r\n * Id(s) of external elements that describe the field, space-separated.\r\n * Forwarded to `aria-describedby` on the trigger.\r\n */\r\n @Input() ariaDescribedBy?: string;\r\n /** Text displayed below the trigger. Shown in red when in error state. */\r\n @Input() supportingText = '';\r\n /**\r\n * Static error text shown in red when in error state. Used when the dropdown is\r\n * outside a form (with `[error]=\"true\"`). When inside a form, prefer `errorMessages`.\r\n */\r\n @Input() errorMessage = '';\r\n /**\r\n * Map of Angular validator error keys to display strings. The component reads\r\n * `ngControl.errors` to determine which validator failed and shows the matching\r\n * message in red below the trigger. Example: `{ required: 'Please choose an option' }`\r\n */\r\n @Input() errorMessages: Record<string, string> = {};\r\n /**\r\n * Adds a blue asterisk after the label. When used in a form, this is set\r\n * automatically if the bound `FormControl` has `Validators.required`.\r\n * Supports boolean coercion.\r\n */\r\n @Input({ transform: booleanAttribute }) required = false;\r\n /**\r\n * Applies the error visual state (red border) explicitly. When used in a form,\r\n * the error state is driven automatically from the `FormControl`'s validity once\r\n * the field is touched — this input is only needed outside of forms. Supports boolean coercion.\r\n */\r\n @Input({ transform: booleanAttribute }) error = false;\r\n /** Disables the dropdown. When used in a form, also responds to `FormControl.disable()`. Supports boolean coercion. */\r\n @Input({ transform: booleanAttribute }) disabled = false;\r\n /** Visually hides the label while keeping it accessible to screen readers. Supports boolean coercion. */\r\n @Input({ transform: booleanAttribute }) hideLabel = false;\r\n\r\n /** Emits the newly selected value whenever the user picks an option. */\r\n @Output() readonly valueChange = new EventEmitter<string>();\r\n /** Emitted when the overlay panel opens. */\r\n @Output() readonly opened = new EventEmitter<void>();\r\n /** Emitted when the overlay panel closes. */\r\n @Output() readonly closed = new EventEmitter<void>();\r\n\r\n protected readonly chevronSvg: SafeHtml;\r\n protected readonly checkSvg: SafeHtml;\r\n\r\n protected isOpen = false;\r\n protected overlayWidth = 0;\r\n protected readonly overlayPositions: ConnectedPosition[] = [\r\n { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },\r\n { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },\r\n ];\r\n\r\n private buildIconSvg(name: string): SafeHtml {\r\n const svg = (LUCIDE_ICONS[name] ?? '').replace('<svg ', '<svg width=\"20\" height=\"20\" ');\r\n return this.sanitizer.bypassSecurityTrustHtml(svg);\r\n }\r\n\r\n get triggerId(): string {\r\n return this.id ?? this._autoId;\r\n }\r\n\r\n get labelId(): string {\r\n return this.triggerId + '-label';\r\n }\r\n\r\n get listboxId(): string {\r\n return this.triggerId + '-listbox';\r\n }\r\n\r\n get selectedOption(): DropdownOption | undefined {\r\n return this.options.find((o) => o.value === this.value);\r\n }\r\n\r\n /** Value array bound to the CDK listbox (single-select, so 0 or 1 entry). */\r\n get listboxValue(): readonly string[] {\r\n return this.value ? [this.value] : [];\r\n }\r\n\r\n /** True when the bound FormControl is invalid and touched, or when `error` is explicitly set. */\r\n get hasError(): boolean {\r\n return this.error || !!(this.ngControl?.invalid && this.ngControl.touched);\r\n }\r\n\r\n /** True when `required` is set explicitly, or when the bound FormControl has Validators.required. */\r\n get isRequired(): boolean {\r\n return this.required || !!this.ngControl?.control?.hasValidator(Validators.required);\r\n }\r\n\r\n private get activeErrorMessage(): string {\r\n const errors = this.ngControl?.errors;\r\n if (!errors) return '';\r\n for (const key of Object.keys(errors)) {\r\n if (this.errorMessages[key]) return this.errorMessages[key];\r\n }\r\n return '';\r\n }\r\n\r\n get displayedSupportingText(): string {\r\n if (this.hasError) {\r\n return this.activeErrorMessage || this.errorMessage || this.supportingText;\r\n }\r\n return this.supportingText;\r\n }\r\n\r\n get describedBy(): string | null {\r\n const parts: string[] = [];\r\n if (this.ariaDescribedBy) parts.push(this.ariaDescribedBy);\r\n if (this.supportingText || this.errorMessage || Object.keys(this.errorMessages).length) {\r\n parts.push(this.triggerId + '-supporting');\r\n }\r\n return parts.length ? parts.join(' ') : null;\r\n }\r\n\r\n get wrapperClasses(): string {\r\n return ['cmp-dropdown', this.size, this.disabled ? 'disabled' : '']\r\n .filter(Boolean)\r\n .join(' ');\r\n }\r\n\r\n // ── Open / close ──────────────────────────────────────────────────────────\r\n\r\n protected toggle(): void {\r\n if (this.isOpen) {\r\n this.close(true);\r\n } else {\r\n this.open();\r\n }\r\n }\r\n\r\n protected open(): void {\r\n if (this.disabled || this.isOpen) return;\r\n this.overlayWidth = this.triggerBtn?.nativeElement.offsetWidth ?? 0;\r\n this.isOpen = true;\r\n this.opened.emit();\r\n this.cdr.markForCheck();\r\n }\r\n\r\n protected close(refocus = false): void {\r\n if (!this.isOpen) return;\r\n this.isOpen = false;\r\n this._onTouched();\r\n this.closed.emit();\r\n if (refocus) {\r\n setTimeout(() => this.triggerBtn?.nativeElement.focus());\r\n }\r\n this.cdr.markForCheck();\r\n }\r\n\r\n protected onPanelAttach(): void {\r\n setTimeout(() => this.listboxEl?.nativeElement.focus());\r\n }\r\n\r\n // ── Keyboard ──────────────────────────────────────────────────────────────\r\n\r\n protected onTriggerKeydown(event: KeyboardEvent): void {\r\n if (this.disabled) return;\r\n if ((event.key === 'ArrowDown' || event.key === 'ArrowUp') && !this.isOpen) {\r\n event.preventDefault();\r\n this.open();\r\n } else if (event.key === 'Escape' && this.isOpen) {\r\n event.preventDefault();\r\n this.close(true);\r\n }\r\n }\r\n\r\n protected onListboxKeydown(event: KeyboardEvent): void {\r\n if (event.key === 'Escape') {\r\n event.preventDefault();\r\n this.close(true);\r\n } else if (event.key === 'Tab') {\r\n event.preventDefault();\r\n this.close(true);\r\n }\r\n }\r\n\r\n protected onListboxValueChange(event: ListboxValueChangeEvent<string>): void {\r\n const next = event.value[0] ?? '';\r\n this.value = next;\r\n this._onChange(next);\r\n this.valueChange.emit(next);\r\n this.close(true);\r\n }\r\n\r\n // ── ControlValueAccessor ────────────────────────────────────────────────────\r\n\r\n writeValue(value: string | null): void {\r\n this.value = value ?? '';\r\n this.cdr.markForCheck();\r\n }\r\n\r\n registerOnChange(fn: (value: string) => void): void {\r\n this._onChange = fn;\r\n }\r\n\r\n registerOnTouched(fn: () => void): void {\r\n this._onTouched = fn;\r\n }\r\n\r\n /** Called by Angular when the bound FormControl is programmatically disabled or enabled. */\r\n setDisabledState(isDisabled: boolean): void {\r\n this.disabled = isDisabled;\r\n this.cdr.markForCheck();\r\n }\r\n}\r\n"]}
package/index.d.ts CHANGED
@@ -7,6 +7,7 @@ import * as i5 from "./lib/sidebar/sidebar";
7
7
  import * as i6 from "./lib/card-base/card";
8
8
  import * as i7 from "./lib/text-input/text-input";
9
9
  import * as i8 from "./lib/dialog/dialog";
10
+ import * as i9 from "./lib/dropdown/dropdown";
10
11
  export { ButtonComponent } from './lib/button/button';
11
12
  export type { ButtonVariant, ButtonSize, ButtonType } from './lib/button/button';
12
13
  export { AccordionComponent } from './lib/accordion/accordion';
@@ -29,8 +30,10 @@ export { DialogService } from './lib/dialog/dialog.service';
29
30
  export type { CompsychDialogConfig } from './lib/dialog/dialog.service';
30
31
  export { CompsychDialogRef } from './lib/dialog/dialog-ref';
31
32
  export { DIALOG_DATA } from '@angular/cdk/dialog';
33
+ export { DropdownComponent } from './lib/dropdown/dropdown';
34
+ export type { DropdownSize, DropdownOption } from './lib/dropdown/dropdown-types';
32
35
  export declare class CompsychAngularModule {
33
36
  static ɵfac: i0.ɵɵFactoryDeclaration<CompsychAngularModule, never>;
34
- static ɵmod: i0.ɵɵNgModuleDeclaration<CompsychAngularModule, never, [typeof i1.ButtonComponent, typeof i2.AccordionComponent, typeof i3.FooterComponent, typeof i4.HeaderNavComponent, typeof i5.SidebarComponent, typeof i6.CardComponent, typeof i7.TextInputComponent, typeof i8.DialogComponent], [typeof i1.ButtonComponent, typeof i2.AccordionComponent, typeof i3.FooterComponent, typeof i4.HeaderNavComponent, typeof i5.SidebarComponent, typeof i6.CardComponent, typeof i7.TextInputComponent, typeof i8.DialogComponent]>;
37
+ static ɵmod: i0.ɵɵNgModuleDeclaration<CompsychAngularModule, never, [typeof i1.ButtonComponent, typeof i2.AccordionComponent, typeof i3.FooterComponent, typeof i4.HeaderNavComponent, typeof i5.SidebarComponent, typeof i6.CardComponent, typeof i7.TextInputComponent, typeof i8.DialogComponent, typeof i9.DropdownComponent], [typeof i1.ButtonComponent, typeof i2.AccordionComponent, typeof i3.FooterComponent, typeof i4.HeaderNavComponent, typeof i5.SidebarComponent, typeof i6.CardComponent, typeof i7.TextInputComponent, typeof i8.DialogComponent, typeof i9.DropdownComponent]>;
35
38
  static ɵinj: i0.ɵɵInjectorDeclaration<CompsychAngularModule>;
36
39
  }
@@ -0,0 +1,11 @@
1
+ /** Size variant of a `compsych-dropdown`. Controls trigger height, padding, and font size. */
2
+ export type DropdownSize = 'small' | 'medium' | 'large';
3
+ /** A single selectable option in a `compsych-dropdown`. */
4
+ export interface DropdownOption {
5
+ /** Value written to the bound form control / emitted by `valueChange`. */
6
+ value: string;
7
+ /** Text shown for the option in the panel and, when selected, in the trigger. */
8
+ label: string;
9
+ /** Prevents the option from being selected or focused. */
10
+ disabled?: boolean;
11
+ }