@compsych-ui-components/angular 7.0.43 → 7.0.45
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.
- package/esm2022/index.js +8 -2
- package/esm2022/index.js.map +1 -1
- package/esm2022/lib/card-base/card.js +2 -2
- package/esm2022/lib/card-base/card.js.map +1 -1
- package/esm2022/lib/dropdown/dropdown-menu.js +135 -0
- package/esm2022/lib/dropdown/dropdown-menu.js.map +1 -0
- package/esm2022/lib/dropdown/dropdown-types.js.map +1 -1
- package/esm2022/lib/dropdown/dropdown.js +73 -25
- package/esm2022/lib/dropdown/dropdown.js.map +1 -1
- package/index.d.ts +4 -2
- package/lib/card-base/card.d.ts +2 -2
- package/lib/dropdown/dropdown-menu.d.ts +41 -0
- package/lib/dropdown/dropdown-types.d.ts +33 -2
- package/lib/dropdown/dropdown.d.ts +26 -5
- package/package.json +1 -1
|
@@ -31,7 +31,6 @@ export class DropdownComponent {
|
|
|
31
31
|
ctrl.valueAccessor = this;
|
|
32
32
|
this.ngControl = ctrl;
|
|
33
33
|
this.chevronSvg = this.buildIconSvg('chevron-down');
|
|
34
|
-
this.checkSvg = this.buildIconSvg('check');
|
|
35
34
|
}
|
|
36
35
|
triggerBtn;
|
|
37
36
|
listboxEl;
|
|
@@ -43,8 +42,8 @@ export class DropdownComponent {
|
|
|
43
42
|
value = '';
|
|
44
43
|
/** Selectable options rendered in the overlay panel. */
|
|
45
44
|
options = [];
|
|
46
|
-
/** Size variant — controls height, padding, and font size. Defaults to `'
|
|
47
|
-
size = '
|
|
45
|
+
/** Size variant — controls height, padding, and font size. Defaults to `'md'`. */
|
|
46
|
+
size = 'md';
|
|
48
47
|
/** Native `id` attribute for the trigger. Auto-generated if omitted. */
|
|
49
48
|
id;
|
|
50
49
|
/**
|
|
@@ -81,8 +80,20 @@ export class DropdownComponent {
|
|
|
81
80
|
disabled = false;
|
|
82
81
|
/** Visually hides the label while keeping it accessible to screen readers. Supports boolean coercion. */
|
|
83
82
|
hideLabel = false;
|
|
84
|
-
/**
|
|
83
|
+
/** Places the label above the trigger (`'top'`, default) or beside it (`'left'`). */
|
|
84
|
+
labelPosition = 'top';
|
|
85
|
+
/**
|
|
86
|
+
* Renders the label as the trigger's own resting text instead of a separate
|
|
87
|
+
* label above/beside it — e.g. the trigger reads "I want to" until an option
|
|
88
|
+
* is chosen. No visible external label is shown, but the `label` is still
|
|
89
|
+
* exposed to assistive tech (a screen-reader-only `<label>` + `aria-labelledby`),
|
|
90
|
+
* so the field is announced as a properly labelled combobox. Supports boolean coercion.
|
|
91
|
+
*/
|
|
92
|
+
inlineLabel = false;
|
|
93
|
+
/** Lucide icon name rendered inside the trigger, before the displayed value/placeholder (e.g. `'globe'`). */
|
|
85
94
|
leadingIcon;
|
|
95
|
+
/** Default Lucide icon shown before every option's label; each option's own `icon` (from the Icon Mapping) overrides it. */
|
|
96
|
+
optionsLeadingIcon;
|
|
86
97
|
/** Emits the newly selected value whenever the user picks an option. */
|
|
87
98
|
valueChange = new EventEmitter();
|
|
88
99
|
/** Emitted when the overlay panel opens. */
|
|
@@ -90,8 +101,8 @@ export class DropdownComponent {
|
|
|
90
101
|
/** Emitted when the overlay panel closes. */
|
|
91
102
|
closed = new EventEmitter();
|
|
92
103
|
chevronSvg;
|
|
93
|
-
checkSvg;
|
|
94
104
|
leadingIconSvg = null;
|
|
105
|
+
optionIconCache = new Map();
|
|
95
106
|
isOpen = false;
|
|
96
107
|
overlayWidth = 0;
|
|
97
108
|
overlayPositions = [
|
|
@@ -105,6 +116,16 @@ export class DropdownComponent {
|
|
|
105
116
|
const svg = (LUCIDE_ICONS[name] ?? '').replace('<svg ', '<svg width="20" height="20" ');
|
|
106
117
|
return this.sanitizer.bypassSecurityTrustHtml(svg);
|
|
107
118
|
}
|
|
119
|
+
/** Cached option-icon SVG by Lucide name (stable reference so option rows don't re-render each cycle). */
|
|
120
|
+
optionIconSvg(name) {
|
|
121
|
+
const key = name ?? '';
|
|
122
|
+
let svg = this.optionIconCache.get(key);
|
|
123
|
+
if (!svg) {
|
|
124
|
+
svg = this.buildIconSvg(key);
|
|
125
|
+
this.optionIconCache.set(key, svg);
|
|
126
|
+
}
|
|
127
|
+
return svg;
|
|
128
|
+
}
|
|
108
129
|
get triggerId() {
|
|
109
130
|
return this.id ?? this._autoId;
|
|
110
131
|
}
|
|
@@ -117,6 +138,14 @@ export class DropdownComponent {
|
|
|
117
138
|
get selectedOption() {
|
|
118
139
|
return this.options.find((o) => o.value === this.value);
|
|
119
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Resting text shown in the trigger: the selected option's label, else the
|
|
143
|
+
* `label` in inline-label mode (so the label reads as the field's own text),
|
|
144
|
+
* else the placeholder.
|
|
145
|
+
*/
|
|
146
|
+
get triggerText() {
|
|
147
|
+
return this.selectedOption?.label || (this.inlineLabel ? this.label : this.placeholder);
|
|
148
|
+
}
|
|
120
149
|
/** Value array bound to the CDK listbox (single-select, so 0 or 1 entry). */
|
|
121
150
|
get listboxValue() {
|
|
122
151
|
return this.value ? [this.value] : [];
|
|
@@ -155,7 +184,7 @@ export class DropdownComponent {
|
|
|
155
184
|
return parts.length ? parts.join(' ') : null;
|
|
156
185
|
}
|
|
157
186
|
get wrapperClasses() {
|
|
158
|
-
return ['cmp-dropdown', this.size, this.disabled ? 'disabled' : '']
|
|
187
|
+
return ['cmp-dropdown', this.size, this.disabled ? 'disabled' : '', this.labelPosition === 'left' ? 'label-left' : '']
|
|
159
188
|
.filter(Boolean)
|
|
160
189
|
.join(' ');
|
|
161
190
|
}
|
|
@@ -237,9 +266,9 @@ export class DropdownComponent {
|
|
|
237
266
|
this.cdr.markForCheck();
|
|
238
267
|
}
|
|
239
268
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DropdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
240
|
-
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], leadingIcon: "leadingIcon" }, outputs: { valueChange: "valueChange", opened: "opened", closed: "closed" }, viewQueries: [{ propertyName: "triggerBtn", first: true, predicate: ["triggerBtn"], descendants: true }, { propertyName: "listboxEl", first: true, predicate: ["listboxEl"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
|
|
269
|
+
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], labelPosition: "labelPosition", inlineLabel: ["inlineLabel", "inlineLabel", booleanAttribute], leadingIcon: "leadingIcon", optionsLeadingIcon: "optionsLeadingIcon" }, outputs: { valueChange: "valueChange", opened: "opened", closed: "closed" }, viewQueries: [{ propertyName: "triggerBtn", first: true, predicate: ["triggerBtn"], descendants: true }, { propertyName: "listboxEl", first: true, predicate: ["listboxEl"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
|
|
241
270
|
<div [class]="wrapperClasses">
|
|
242
|
-
<div class="label-row" [class.sr-only]="hideLabel">
|
|
271
|
+
<div class="label-row" [class.sr-only]="hideLabel || inlineLabel">
|
|
243
272
|
<label class="label" [id]="labelId" [for]="triggerId">
|
|
244
273
|
{{ label }}
|
|
245
274
|
@if (isRequired) {
|
|
@@ -255,7 +284,7 @@ export class DropdownComponent {
|
|
|
255
284
|
type="button"
|
|
256
285
|
class="trigger"
|
|
257
286
|
[id]="triggerId"
|
|
258
|
-
[class.placeholder]="!selectedOption"
|
|
287
|
+
[class.placeholder]="!selectedOption && !inlineLabel"
|
|
259
288
|
[class.error]="hasError"
|
|
260
289
|
[class.open]="isOpen"
|
|
261
290
|
[disabled]="disabled"
|
|
@@ -273,7 +302,7 @@ export class DropdownComponent {
|
|
|
273
302
|
@if (leadingIconSvg) {
|
|
274
303
|
<span class="trigger-icon" aria-hidden="true" [innerHTML]="leadingIconSvg"></span>
|
|
275
304
|
}
|
|
276
|
-
<span class="trigger-text">{{
|
|
305
|
+
<span class="trigger-text">{{ triggerText }}</span>
|
|
277
306
|
<span class="trigger-chevron" aria-hidden="true" [innerHTML]="chevronSvg"></span>
|
|
278
307
|
</button>
|
|
279
308
|
|
|
@@ -299,6 +328,7 @@ export class DropdownComponent {
|
|
|
299
328
|
#listboxEl
|
|
300
329
|
cdkListbox
|
|
301
330
|
class="panel"
|
|
331
|
+
[style.width.px]="overlayWidth"
|
|
302
332
|
[id]="listboxId"
|
|
303
333
|
[attr.aria-labelledby]="labelId"
|
|
304
334
|
[cdkListboxValue]="listboxValue"
|
|
@@ -306,24 +336,29 @@ export class DropdownComponent {
|
|
|
306
336
|
(keydown)="onListboxKeydown($event)"
|
|
307
337
|
>
|
|
308
338
|
@for (opt of options; track opt.value) {
|
|
309
|
-
<li class="option" [cdkOption]="opt.value" [cdkOptionDisabled]="!!opt.disabled">
|
|
310
|
-
<span class="option-
|
|
311
|
-
@if (opt.
|
|
312
|
-
<span [innerHTML]="
|
|
339
|
+
<li class="option" [class.error]="opt.error" [cdkOption]="opt.value" [cdkOptionDisabled]="!!opt.disabled">
|
|
340
|
+
<span class="option-inner">
|
|
341
|
+
@if (opt.icon || optionsLeadingIcon) {
|
|
342
|
+
<span class="option-icon" aria-hidden="true" [innerHTML]="optionIconSvg(opt.icon || optionsLeadingIcon)"></span>
|
|
313
343
|
}
|
|
344
|
+
<span class="option-text">
|
|
345
|
+
<span class="option-label">{{ opt.label }}</span>
|
|
346
|
+
@if (opt.supportingText) {
|
|
347
|
+
<span class="option-support">{{ opt.supportingText }}</span>
|
|
348
|
+
}
|
|
349
|
+
</span>
|
|
314
350
|
</span>
|
|
315
|
-
<span class="option-label">{{ opt.label }}</span>
|
|
316
351
|
</li>
|
|
317
352
|
}
|
|
318
353
|
</ul>
|
|
319
354
|
</ng-template>
|
|
320
|
-
`, 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.
|
|
355
|
+
`, isInline: true, styles: [":host{display:block}.cmp-dropdown{display:flex;flex-direction:column;gap:var(--spacing-padding-sys-padding-8)}.cmp-dropdown.label-left{display:grid;grid-template-columns:auto minmax(0,1fr);grid-template-areas:\"label field\" \". support\";align-items:center;column-gap:var(--spacing-padding-sys-padding-12);row-gap:var(--spacing-padding-sys-padding-4)}.cmp-dropdown.label-left .label-row{grid-area:label}.cmp-dropdown.label-left .trigger{grid-area:field}.cmp-dropdown.label-left .supporting-text{grid-area:support}.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.lg .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:.64}.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-icon{display:flex;align-items:center;flex-shrink:0;color:var(--surface-surface-sys-on-surface-variant)}.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:color .15s ease,transform .16s ease}.trigger:focus-visible .trigger-chevron,.trigger.open .trigger-chevron{color:var(--accent-primary-sys-primary)}.trigger.open .trigger-chevron{transform:rotate(180deg)}.trigger.error:not(:disabled) .trigger-chevron{color:var(--error-sys-error)}@media(prefers-reduced-motion:reduce){.trigger-chevron{transition:color .15s ease}}.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}.trigger:disabled .trigger-text,.trigger:disabled .trigger-icon,.trigger:disabled .trigger-chevron{opacity:.64}.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:.64}.cmp-dropdown.sm .trigger{height:40px;gap:var(--spacing-padding-sys-padding-4);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.md .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.lg .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) 0;max-height:340px;overflow-x:hidden;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-md);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:block;padding:var(--spacing-padding-sys-padding-4) var(--spacing-padding-sys-padding-8);cursor:pointer;outline:none;-webkit-user-select:none;user-select:none}.option-inner{display:flex;align-items:flex-start;gap:var(--spacing-padding-sys-padding-12);padding:var(--spacing-padding-sys-padding-8);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);transition:background-color .12s ease-out}.option-icon{display:flex;align-items:center;justify-content:center;flex-shrink:0;height:20px;color:var(--surface-surface-sys-on-surface);margin-top:var(--spacing-padding-sys-padding-2)}.option-text{flex:1 1 auto;min-width:0;display:flex;align-items:baseline;flex-wrap:wrap;column-gap:var(--spacing-padding-sys-padding-8);row-gap:var(--spacing-padding-sys-padding-2)}.option-label{min-width:0}.option-support{color:var(--surface-surface-sys-on-surface-variant);font-size:var(--body-small-sys-font-size);line-height:var(--body-small-sys-line-height)}.option.error .option-icon,.option.error .option-label{color:var(--error-sys-error)}.option:hover:not([aria-disabled=true]) .option-inner{background-color:color-mix(in srgb,var(--surface-surface-sys-on-surface-variant) 8%,transparent)}.option:focus-visible{outline:none}.option:focus-visible .option-inner{background-color:color-mix(in srgb,var(--surface-surface-sys-on-surface-variant) 8%,transparent);outline:var(--border-width-sys-stroke-medium) solid var(--accent-primary-sys-primary);outline-offset:-2px}.option[aria-disabled=true]{cursor:not-allowed}.option[aria-disabled=true] .option-inner{color:var(--surface-surface-sys-on-surface-variant);opacity:.64}@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:focus-visible .option-inner{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 });
|
|
321
356
|
}
|
|
322
357
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DropdownComponent, decorators: [{
|
|
323
358
|
type: Component,
|
|
324
359
|
args: [{ selector: 'compsych-dropdown', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CdkListbox, CdkOption, CdkConnectedOverlay, CdkOverlayOrigin], template: `
|
|
325
360
|
<div [class]="wrapperClasses">
|
|
326
|
-
<div class="label-row" [class.sr-only]="hideLabel">
|
|
361
|
+
<div class="label-row" [class.sr-only]="hideLabel || inlineLabel">
|
|
327
362
|
<label class="label" [id]="labelId" [for]="triggerId">
|
|
328
363
|
{{ label }}
|
|
329
364
|
@if (isRequired) {
|
|
@@ -339,7 +374,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
339
374
|
type="button"
|
|
340
375
|
class="trigger"
|
|
341
376
|
[id]="triggerId"
|
|
342
|
-
[class.placeholder]="!selectedOption"
|
|
377
|
+
[class.placeholder]="!selectedOption && !inlineLabel"
|
|
343
378
|
[class.error]="hasError"
|
|
344
379
|
[class.open]="isOpen"
|
|
345
380
|
[disabled]="disabled"
|
|
@@ -357,7 +392,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
357
392
|
@if (leadingIconSvg) {
|
|
358
393
|
<span class="trigger-icon" aria-hidden="true" [innerHTML]="leadingIconSvg"></span>
|
|
359
394
|
}
|
|
360
|
-
<span class="trigger-text">{{
|
|
395
|
+
<span class="trigger-text">{{ triggerText }}</span>
|
|
361
396
|
<span class="trigger-chevron" aria-hidden="true" [innerHTML]="chevronSvg"></span>
|
|
362
397
|
</button>
|
|
363
398
|
|
|
@@ -383,6 +418,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
383
418
|
#listboxEl
|
|
384
419
|
cdkListbox
|
|
385
420
|
class="panel"
|
|
421
|
+
[style.width.px]="overlayWidth"
|
|
386
422
|
[id]="listboxId"
|
|
387
423
|
[attr.aria-labelledby]="labelId"
|
|
388
424
|
[cdkListboxValue]="listboxValue"
|
|
@@ -390,18 +426,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
390
426
|
(keydown)="onListboxKeydown($event)"
|
|
391
427
|
>
|
|
392
428
|
@for (opt of options; track opt.value) {
|
|
393
|
-
<li class="option" [cdkOption]="opt.value" [cdkOptionDisabled]="!!opt.disabled">
|
|
394
|
-
<span class="option-
|
|
395
|
-
@if (opt.
|
|
396
|
-
<span [innerHTML]="
|
|
429
|
+
<li class="option" [class.error]="opt.error" [cdkOption]="opt.value" [cdkOptionDisabled]="!!opt.disabled">
|
|
430
|
+
<span class="option-inner">
|
|
431
|
+
@if (opt.icon || optionsLeadingIcon) {
|
|
432
|
+
<span class="option-icon" aria-hidden="true" [innerHTML]="optionIconSvg(opt.icon || optionsLeadingIcon)"></span>
|
|
397
433
|
}
|
|
434
|
+
<span class="option-text">
|
|
435
|
+
<span class="option-label">{{ opt.label }}</span>
|
|
436
|
+
@if (opt.supportingText) {
|
|
437
|
+
<span class="option-support">{{ opt.supportingText }}</span>
|
|
438
|
+
}
|
|
439
|
+
</span>
|
|
398
440
|
</span>
|
|
399
|
-
<span class="option-label">{{ opt.label }}</span>
|
|
400
441
|
</li>
|
|
401
442
|
}
|
|
402
443
|
</ul>
|
|
403
444
|
</ng-template>
|
|
404
|
-
`, 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.
|
|
445
|
+
`, styles: [":host{display:block}.cmp-dropdown{display:flex;flex-direction:column;gap:var(--spacing-padding-sys-padding-8)}.cmp-dropdown.label-left{display:grid;grid-template-columns:auto minmax(0,1fr);grid-template-areas:\"label field\" \". support\";align-items:center;column-gap:var(--spacing-padding-sys-padding-12);row-gap:var(--spacing-padding-sys-padding-4)}.cmp-dropdown.label-left .label-row{grid-area:label}.cmp-dropdown.label-left .trigger{grid-area:field}.cmp-dropdown.label-left .supporting-text{grid-area:support}.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.lg .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:.64}.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-icon{display:flex;align-items:center;flex-shrink:0;color:var(--surface-surface-sys-on-surface-variant)}.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:color .15s ease,transform .16s ease}.trigger:focus-visible .trigger-chevron,.trigger.open .trigger-chevron{color:var(--accent-primary-sys-primary)}.trigger.open .trigger-chevron{transform:rotate(180deg)}.trigger.error:not(:disabled) .trigger-chevron{color:var(--error-sys-error)}@media(prefers-reduced-motion:reduce){.trigger-chevron{transition:color .15s ease}}.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}.trigger:disabled .trigger-text,.trigger:disabled .trigger-icon,.trigger:disabled .trigger-chevron{opacity:.64}.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:.64}.cmp-dropdown.sm .trigger{height:40px;gap:var(--spacing-padding-sys-padding-4);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.md .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.lg .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) 0;max-height:340px;overflow-x:hidden;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-md);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:block;padding:var(--spacing-padding-sys-padding-4) var(--spacing-padding-sys-padding-8);cursor:pointer;outline:none;-webkit-user-select:none;user-select:none}.option-inner{display:flex;align-items:flex-start;gap:var(--spacing-padding-sys-padding-12);padding:var(--spacing-padding-sys-padding-8);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);transition:background-color .12s ease-out}.option-icon{display:flex;align-items:center;justify-content:center;flex-shrink:0;height:20px;color:var(--surface-surface-sys-on-surface);margin-top:var(--spacing-padding-sys-padding-2)}.option-text{flex:1 1 auto;min-width:0;display:flex;align-items:baseline;flex-wrap:wrap;column-gap:var(--spacing-padding-sys-padding-8);row-gap:var(--spacing-padding-sys-padding-2)}.option-label{min-width:0}.option-support{color:var(--surface-surface-sys-on-surface-variant);font-size:var(--body-small-sys-font-size);line-height:var(--body-small-sys-line-height)}.option.error .option-icon,.option.error .option-label{color:var(--error-sys-error)}.option:hover:not([aria-disabled=true]) .option-inner{background-color:color-mix(in srgb,var(--surface-surface-sys-on-surface-variant) 8%,transparent)}.option:focus-visible{outline:none}.option:focus-visible .option-inner{background-color:color-mix(in srgb,var(--surface-surface-sys-on-surface-variant) 8%,transparent);outline:var(--border-width-sys-stroke-medium) solid var(--accent-primary-sys-primary);outline-offset:-2px}.option[aria-disabled=true]{cursor:not-allowed}.option[aria-disabled=true] .option-inner{color:var(--surface-surface-sys-on-surface-variant);opacity:.64}@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:focus-visible .option-inner{outline:2px solid Highlight;outline-offset:-2px}}\n"] }]
|
|
405
446
|
}], ctorParameters: () => [], propDecorators: { triggerBtn: [{
|
|
406
447
|
type: ViewChild,
|
|
407
448
|
args: ['triggerBtn']
|
|
@@ -440,8 +481,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
440
481
|
}], hideLabel: [{
|
|
441
482
|
type: Input,
|
|
442
483
|
args: [{ transform: booleanAttribute }]
|
|
484
|
+
}], labelPosition: [{
|
|
485
|
+
type: Input
|
|
486
|
+
}], inlineLabel: [{
|
|
487
|
+
type: Input,
|
|
488
|
+
args: [{ transform: booleanAttribute }]
|
|
443
489
|
}], leadingIcon: [{
|
|
444
490
|
type: Input
|
|
491
|
+
}], optionsLeadingIcon: [{
|
|
492
|
+
type: Input
|
|
445
493
|
}], valueChange: [{
|
|
446
494
|
type: Output
|
|
447
495
|
}], opened: [{
|
|
@@ -1 +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 OnChanges,\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 @if (leadingIconSvg) {\r\n <span class=\"trigger-icon\" aria-hidden=\"true\" [innerHTML]=\"leadingIconSvg\"></span>\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, OnChanges {\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 /** Lucide icon name rendered inside the trigger before the text (e.g. `'globe'`). */\r\n @Input() leadingIcon?: string;\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 protected leadingIconSvg: SafeHtml | null = null;\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 ngOnChanges(): void {\r\n this.leadingIconSvg = this.leadingIcon ? this.buildIconSvg(this.leadingIcon) : null;\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"]}
|
|
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 OnChanges,\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 { DropdownLabelPosition, 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 || inlineLabel\">\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 && !inlineLabel\"\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 @if (leadingIconSvg) {\r\n <span class=\"trigger-icon\" aria-hidden=\"true\" [innerHTML]=\"leadingIconSvg\"></span>\r\n }\r\n <span class=\"trigger-text\">{{ triggerText }}</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 [style.width.px]=\"overlayWidth\"\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\" [class.error]=\"opt.error\" [cdkOption]=\"opt.value\" [cdkOptionDisabled]=\"!!opt.disabled\">\r\n <span class=\"option-inner\">\r\n @if (opt.icon || optionsLeadingIcon) {\r\n <span class=\"option-icon\" aria-hidden=\"true\" [innerHTML]=\"optionIconSvg(opt.icon || optionsLeadingIcon)\"></span>\r\n }\r\n <span class=\"option-text\">\r\n <span class=\"option-label\">{{ opt.label }}</span>\r\n @if (opt.supportingText) {\r\n <span class=\"option-support\">{{ opt.supportingText }}</span>\r\n }\r\n </span>\r\n </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, OnChanges {\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 }\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 `'md'`. */\r\n @Input() size: DropdownSize = 'md';\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 /** Places the label above the trigger (`'top'`, default) or beside it (`'left'`). */\r\n @Input() labelPosition: DropdownLabelPosition = 'top';\r\n /**\r\n * Renders the label as the trigger's own resting text instead of a separate\r\n * label above/beside it — e.g. the trigger reads \"I want to\" until an option\r\n * is chosen. No visible external label is shown, but the `label` is still\r\n * exposed to assistive tech (a screen-reader-only `<label>` + `aria-labelledby`),\r\n * so the field is announced as a properly labelled combobox. Supports boolean coercion.\r\n */\r\n @Input({ transform: booleanAttribute }) inlineLabel = false;\r\n /** Lucide icon name rendered inside the trigger, before the displayed value/placeholder (e.g. `'globe'`). */\r\n @Input() leadingIcon?: string;\r\n /** Default Lucide icon shown before every option's label; each option's own `icon` (from the Icon Mapping) overrides it. */\r\n @Input() optionsLeadingIcon?: string;\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 leadingIconSvg: SafeHtml | null = null;\r\n private readonly optionIconCache = new Map<string, 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 ngOnChanges(): void {\r\n this.leadingIconSvg = this.leadingIcon ? this.buildIconSvg(this.leadingIcon) : null;\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 /** Cached option-icon SVG by Lucide name (stable reference so option rows don't re-render each cycle). */\r\n protected optionIconSvg(name: string | undefined): SafeHtml {\r\n const key = name ?? '';\r\n let svg = this.optionIconCache.get(key);\r\n if (!svg) {\r\n svg = this.buildIconSvg(key);\r\n this.optionIconCache.set(key, svg);\r\n }\r\n return 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 /**\r\n * Resting text shown in the trigger: the selected option's label, else the\r\n * `label` in inline-label mode (so the label reads as the field's own text),\r\n * else the placeholder.\r\n */\r\n get triggerText(): string {\r\n return this.selectedOption?.label || (this.inlineLabel ? this.label : this.placeholder);\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' : '', this.labelPosition === 'left' ? 'label-left' : '']\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
|
@@ -8,6 +8,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
10
|
import * as i9 from "./lib/dropdown/dropdown";
|
|
11
|
+
import * as i10 from "./lib/dropdown/dropdown-menu";
|
|
11
12
|
export { ButtonComponent } from './lib/button/button';
|
|
12
13
|
export type { ButtonVariant, ButtonSize, ButtonType } from './lib/button/button';
|
|
13
14
|
export { AccordionComponent } from './lib/accordion/accordion';
|
|
@@ -31,9 +32,10 @@ export type { CompsychDialogConfig } from './lib/dialog/dialog.service';
|
|
|
31
32
|
export { CompsychDialogRef } from './lib/dialog/dialog-ref';
|
|
32
33
|
export { DIALOG_DATA } from '@angular/cdk/dialog';
|
|
33
34
|
export { DropdownComponent } from './lib/dropdown/dropdown';
|
|
34
|
-
export
|
|
35
|
+
export { DropdownMenuComponent } from './lib/dropdown/dropdown-menu';
|
|
36
|
+
export type { DropdownSize, DropdownOption, DropdownLabelPosition, DropdownMenuKind, DropdownMenuItem, } from './lib/dropdown/dropdown-types';
|
|
35
37
|
export declare class CompsychAngularModule {
|
|
36
38
|
static ɵfac: i0.ɵɵFactoryDeclaration<CompsychAngularModule, never>;
|
|
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]>;
|
|
39
|
+
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 i10.DropdownMenuComponent], [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 i10.DropdownMenuComponent]>;
|
|
38
40
|
static ɵinj: i0.ɵɵInjectorDeclaration<CompsychAngularModule>;
|
|
39
41
|
}
|
package/lib/card-base/card.d.ts
CHANGED
|
@@ -179,9 +179,9 @@ export declare class CardComponent {
|
|
|
179
179
|
*/
|
|
180
180
|
readonly effectiveHeadingLevel: import("@angular/core").Signal<CardHeadingLevel | undefined>;
|
|
181
181
|
/** Base Card Item plain-icon glyph size by `size` (48 md, 32 sm, 24 xs). */
|
|
182
|
-
readonly basicIconSize: import("@angular/core").Signal<
|
|
182
|
+
readonly basicIconSize: import("@angular/core").Signal<24 | 32 | 48>;
|
|
183
183
|
/** Base Card Item arrow glyph size by `size` (24 md, 20 sm/xs). */
|
|
184
|
-
readonly basicArrowSize: import("@angular/core").Signal<
|
|
184
|
+
readonly basicArrowSize: import("@angular/core").Signal<20 | 24>;
|
|
185
185
|
/** CSS `url(...)` for the media layer, or null when no image is set. */
|
|
186
186
|
readonly mediaUrl: import("@angular/core").Signal<string | null>;
|
|
187
187
|
/** Padding resolved per variant; `basic` honors the `padding` input. */
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { EventEmitter } from '@angular/core';
|
|
2
|
+
import { SafeHtml } from '@angular/platform-browser';
|
|
3
|
+
import { DropdownMenuItem, DropdownMenuKind } from './dropdown-types';
|
|
4
|
+
import * as i0 from "@angular/core";
|
|
5
|
+
/**
|
|
6
|
+
* The standalone popover surface of the dropdown — a `surface-container-lowest`
|
|
7
|
+
* card at `radius-md`/elevation-2 holding a 4px-padded stack of 48px rows (icon,
|
|
8
|
+
* label, optional inline supporting text). It serves two ARIA contracts: an
|
|
9
|
+
* action `menu` (rows are `menuitem`s) and a select `listbox` (rows are
|
|
10
|
+
* `option`s with `aria-selected`).
|
|
11
|
+
*
|
|
12
|
+
* The consumer owns visibility and positioning (render it inline, or inside a
|
|
13
|
+
* CDK overlay). `compsych-dropdown` composes the same visual language for the
|
|
14
|
+
* panel it opens; this component exposes it for standalone action menus.
|
|
15
|
+
*/
|
|
16
|
+
export declare class DropdownMenuComponent {
|
|
17
|
+
private readonly sanitizer;
|
|
18
|
+
/** Rows to render. */
|
|
19
|
+
items: DropdownMenuItem[];
|
|
20
|
+
/** ARIA contract: `'menu'` (default, action menu) or `'listbox'` (select). */
|
|
21
|
+
kind: DropdownMenuKind;
|
|
22
|
+
/** In `listbox` mode, the id (or label) of the currently-selected row. */
|
|
23
|
+
selectedId?: string;
|
|
24
|
+
/** Default Lucide icon shown before every row's label; each item's own `icon` (from the Icon Mapping) overrides it. */
|
|
25
|
+
leadingIcon?: string;
|
|
26
|
+
/** Denser variant — one type-ramp step down, rows hug to 44px. Supports boolean coercion. */
|
|
27
|
+
compact: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Accessible name for the list. Required in `listbox` mode when no external
|
|
30
|
+
* labelled control owns the menu (a nameless listbox fails accessibility checks).
|
|
31
|
+
*/
|
|
32
|
+
ariaLabel?: string;
|
|
33
|
+
/** Emits the chosen row (disabled rows never emit). */
|
|
34
|
+
readonly itemSelect: EventEmitter<DropdownMenuItem>;
|
|
35
|
+
private readonly iconCache;
|
|
36
|
+
protected iconSvg(name: string | undefined): SafeHtml;
|
|
37
|
+
protected select(item: DropdownMenuItem): void;
|
|
38
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<DropdownMenuComponent, never>;
|
|
39
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<DropdownMenuComponent, "compsych-dropdown-menu", never, { "items": { "alias": "items"; "required": false; }; "kind": { "alias": "kind"; "required": false; }; "selectedId": { "alias": "selectedId"; "required": false; }; "leadingIcon": { "alias": "leadingIcon"; "required": false; }; "compact": { "alias": "compact"; "required": false; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; }; }, { "itemSelect": "itemSelect"; }, never, never, true, never>;
|
|
40
|
+
static ngAcceptInputType_compact: unknown;
|
|
41
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
/** Size variant of a `compsych-dropdown
|
|
2
|
-
export type DropdownSize = '
|
|
1
|
+
/** Size variant of a `compsych-dropdown` (40 / 48 / 56px). Controls trigger height, padding, and font size. */
|
|
2
|
+
export type DropdownSize = 'sm' | 'md' | 'lg';
|
|
3
|
+
/** Placement of the label relative to the trigger. `'top'` stacks it above (default); `'left'` sets it beside the trigger. */
|
|
4
|
+
export type DropdownLabelPosition = 'top' | 'left';
|
|
3
5
|
/** A single selectable option in a `compsych-dropdown`. */
|
|
4
6
|
export interface DropdownOption {
|
|
5
7
|
/** Value written to the bound form control / emitted by `valueChange`. */
|
|
@@ -8,4 +10,33 @@ export interface DropdownOption {
|
|
|
8
10
|
label: string;
|
|
9
11
|
/** Prevents the option from being selected or focused. */
|
|
10
12
|
disabled?: boolean;
|
|
13
|
+
/** Optional secondary text shown after the label (drops to its own line when the row wraps). */
|
|
14
|
+
supportingText?: string;
|
|
15
|
+
/** Retones the option's label to `sys-error` (e.g. a destructive choice). */
|
|
16
|
+
error?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Per-option Lucide icon name, assigned from the design system's Icon Mapping
|
|
19
|
+
* (use → glyph). Overrides the dropdown's `optionsLeadingIcon` default for this row.
|
|
20
|
+
*/
|
|
21
|
+
icon?: string;
|
|
22
|
+
}
|
|
23
|
+
/** ARIA contract for a standalone `compsych-dropdown-menu`: an action `menu` or a select `listbox`. */
|
|
24
|
+
export type DropdownMenuKind = 'menu' | 'listbox';
|
|
25
|
+
/** A single row in a standalone `compsych-dropdown-menu`. */
|
|
26
|
+
export interface DropdownMenuItem {
|
|
27
|
+
/** Stable id emitted on select and matched against the listbox selection. Falls back to `label`. */
|
|
28
|
+
id?: string;
|
|
29
|
+
/** Row text. */
|
|
30
|
+
label: string;
|
|
31
|
+
/** Optional secondary text shown after the label (drops to its own line when the row wraps). */
|
|
32
|
+
supportingText?: string;
|
|
33
|
+
/** Prevents the row from being chosen or focused. */
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
/** Retones the row's label to `sys-error` (e.g. a destructive action). */
|
|
36
|
+
error?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Per-row Lucide icon name, assigned from the design system's Icon Mapping
|
|
39
|
+
* (use → glyph). Overrides the menu's `leadingIcon` default for this row.
|
|
40
|
+
*/
|
|
41
|
+
icon?: string;
|
|
11
42
|
}
|