import { AfterViewInit, ChangeDetectionStrategy, Component, computed, contentChildren, effect, ElementRef, inject, input, model, signal, } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { NgIconComponent, provideIcons } from '@ng-icons/core'; import { isaActionChevronUp, isaActionChevronDown } from '@isa/icons'; import { ActiveDescendantKeyManager, Highlightable } from '@angular/cdk/a11y'; import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay'; import { isEqual } from 'lodash'; import { DropdownAppearance } from './dropdown.types'; @Component({ selector: 'ui-dropdown-option', template: '', host: { '[class]': '["ui-dropdown-option", activeClass(), selectedClass()]', 'role': 'option', '[attr.aria-selected]': 'selected()', '[attr.tabindex]': '-1', '(click)': 'select()', }, }) export class DropdownOptionComponent implements Highlightable { private host = inject(DropdownButtonComponent); private elementRef = inject(ElementRef); active = signal(false); activeClass = computed(() => (this.active() ? 'active' : '')); setActiveStyles(): void { this.active.set(true); } setInactiveStyles(): void { this.active.set(false); } getLabel(): string { return this.elementRef.nativeElement.textContent.trim(); } selected = computed(() => { const hostValue = this.host.value(); const value = this.value(); return hostValue === value || isEqual(hostValue, value); }); selectedClass = computed(() => (this.selected() ? 'selected' : '')); value = input.required(); select() { this.host.select(this); this.host.close(); } } @Component({ selector: 'ui-dropdown', templateUrl: './dropdown.component.html', changeDetection: ChangeDetectionStrategy.OnPush, hostDirectives: [CdkOverlayOrigin], imports: [NgIconComponent, CdkConnectedOverlay], providers: [ provideIcons({ isaActionChevronUp, isaActionChevronDown }), { provide: NG_VALUE_ACCESSOR, useExisting: DropdownButtonComponent, multi: true }, ], host: { '[class]': '["ui-dropdown", appearanceClass(), isOpenClass()]', 'role': 'combobox', 'aria-haspopup': 'listbox', '[attr.id]': 'id()', '[attr.tabindex]': 'disabled() ? -1 : tabIndex()', 'aria-expanded': 'isOpen()', '(keydown)': 'keyManger?.onKeydown($event)', '(keydown.enter)': 'select(keyManger.activeItem); close()', '(keydown.escape)': 'close()', '(click)': 'isOpen() ? close() : open()', }, }) export class DropdownButtonComponent implements ControlValueAccessor, AfterViewInit { readonly init = signal(false); private elementRef = inject(ElementRef); get overlayMinWidth() { return this.elementRef.nativeElement.offsetWidth; } appearance = input(DropdownAppearance.AccentOutline); appearanceClass = computed(() => `ui-dropdown__${this.appearance()}`); id = input(); value = model(); tabIndex = input(0); label = input(); disabled = model(false); showSelectedValue = input(true); options = contentChildren(DropdownOptionComponent); cdkOverlayOrigin = inject(CdkOverlayOrigin, { self: true }); selectedOption = computed(() => { const options = this.options(); if (!options) { return undefined; } return options.find((option) => option.value() === this.value()); }); private keyManger?: ActiveDescendantKeyManager>; onChange?: (value: T) => void; onTouched?: () => void; isOpen = signal(false); isOpenClass = computed(() => (this.isOpen() ? 'open' : '')); isOpenIcon = computed(() => (this.isOpen() ? 'isaActionChevronUp' : 'isaActionChevronDown')); viewLabel = computed(() => { if (!this.showSelectedValue()) { return this.label() ?? this.value(); } const selectedOption = this.selectedOption(); if (!selectedOption) { return this.label() ?? this.value(); } return selectedOption.getLabel(); }); constructor() { effect(() => { if (!this.init()) { return; } this.keyManger?.destroy(); this.keyManger = new ActiveDescendantKeyManager>( this.options(), ).withWrap(); }); } open() { const selected = this.selectedOption(); if (selected) { this.keyManger?.setActiveItem(selected); } else { this.keyManger?.setFirstItemActive(); } this.isOpen.set(true); } close() { this.isOpen.set(false); } focusout() { // this.close(); } ngAfterViewInit(): void { this.init.set(true); } writeValue(obj: unknown): void { this.value.set(obj as T); } registerOnChange(fn: unknown): void { this.onChange = fn as (value: T) => void; } registerOnTouched(fn: unknown): void { this.onTouched = fn as () => void; } setDisabledState?(isDisabled: boolean): void { this.disabled.set(isDisabled); } select(option: DropdownOptionComponent, options: { emit: boolean } = { emit: true }) { this.value.set(option.value()); if (options.emit) { this.onChange?.(option.value()); } this.onTouched?.(); } }