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?.();
}
}