diff --git a/src/app/components/dropdown/dropdown.html b/src/app/components/dropdown/dropdown.html
index 3359dd7..24dc812 100644
--- a/src/app/components/dropdown/dropdown.html
+++ b/src/app/components/dropdown/dropdown.html
@@ -1,24 +1,30 @@
+
+
+
+
@if (icon()) {
{{ icon() }}
}
- @if (isOpen()) {
-
{{ this.initialInputText() }}
- } @else {
+ @if (recreateKey() % 2 == 0) {
}
+ @else {
}
+
+ @if (!isOpen()) {
{{ selectedLabel() }}
}
keyboard_arrow_down
@if (isOpen()) {
-
-
+
+
@for (option of filteredOptions(); track option.value; let i = $index) {
- = 0 ? dropdownId + '-option-' + focusedIndex() : null",
+ '[attr.aria-activedescendant]': 'ariaActiveDescendant()',
'[attr.aria-label]': 'ariaLabel()',
'[class.open]': 'isOpen()',
tabindex: '0',
@@ -50,7 +51,9 @@ export interface DropdownOption {
})
export class Dropdown implements OnInit {
translation = inject(TranslationService).translation;
+ private platformId = inject(PLATFORM_ID);
searchInput = viewChild>('searchInput');
+ optionItems = viewChildren>('optionItem');
options = input>([]);
placeholder = input('Select an option');
@@ -64,6 +67,13 @@ export class Dropdown implements OnInit {
inputFocusMode = signal(false);
focusedIndex = signal(-1);
searchQuery = signal('');
+ recreateKey = signal(0);
+
+ ariaActiveDescendant = computed(() => {
+ return this.isOpen() && this.focusedIndex() >= 0
+ ? this.dropdownId + '-option-' + this.focusedIndex()
+ : null;
+ });
private static nextId = 0;
private isLatestMouseDownInsideInput = false;
@@ -73,12 +83,31 @@ export class Dropdown implements OnInit {
constructor(private element: ElementRef) {
effect(() => this.filteredOptions());
+ effect(() => {
+ const index = this.focusedIndex();
+ const isDropdownOpen = this.isOpen();
+
+ if (isDropdownOpen && index >= 0) {
+ requestAnimationFrame(() => {
+ this.scrollToIndex(index);
+ })
+ }
+ });
+
effect(() => {
const isDropdownOpen = this.isOpen();
if (!isDropdownOpen) {
if (untracked(this.inputFocusMode)) this.inputFocusMode.set(false);
this.searchQuery.set('');
+
+ const input = this.searchInput();
+ if (isPlatformBrowser(this.platformId) && input) {
+ let currentValue = untracked(this.normalizedOptions).find(
+ (o) => o.value === this.value(),
+ );
+ if (currentValue) input.nativeElement.innerText = currentValue.label;
+ }
}
});
}
@@ -93,6 +122,17 @@ export class Dropdown implements OnInit {
}
}
+ private scrollToIndex(index: number) {
+ if (!isPlatformBrowser(this.platformId)) return;
+
+ const items = this.optionItems();
+ if (items[index]) {
+ items[index].nativeElement.scrollIntoView({
+ block: 'nearest',
+ });
+ }
+ }
+
normalizedOptions = computed(() => {
return this.options().map((opt) => {
if (typeof opt === 'string') {
@@ -112,30 +152,25 @@ export class Dropdown implements OnInit {
return opts.filter((o) => o.label.toLowerCase().normalize('NFKD').includes(query));
});
- initialInputText = computed(() => {
- let currentValue = this.normalizedOptions().find((o) => o.value === this.value())?.label;
- return currentValue ? currentValue : "";
- })
-
toggle() {
if (this.isLatestMouseDownInsideInput) {
this.isLatestMouseDownInsideInput = false;
return;
}
- this.isOpen.update((v) => !v);
if (this.isOpen()) {
+ this.close();
+ } else {
+ this.isOpen.set(true);
this.searchQuery.set('');
+ this.focusedIndex.set(this.normalizedOptions().findIndex((o) => o.value === this.value()));
- const currentIndex = this.normalizedOptions().findIndex((o) => o.value === this.value());
- this.focusedIndex.set(currentIndex);
-
- setTimeout(() => {
+ requestAnimationFrame(() => {
const searchInputElement = this.searchInput()!.nativeElement;
searchInputElement.focus();
this.setCursorToEnd(searchInputElement);
- }, 0);
+ });
}
}
@@ -156,7 +191,13 @@ export class Dropdown implements OnInit {
}
close() {
+ const isFocusInside = this.element.nativeElement.contains(document.activeElement);
+ const isDropdownOpen = this.isOpen();
+
this.isOpen.set(false);
+ if (isFocusInside) this.element.nativeElement.focus({ preventScroll: true });
+ if (isDropdownOpen) this.recreateKey.update((v) => (v + 1) % 65536);
+
this.focusedIndex.set(-1);
}
@@ -248,7 +289,8 @@ export class Dropdown implements OnInit {
onDocumentClick(event: MouseEvent) {
if (
!this.element.nativeElement.contains(event.target) &&
- !this.isLatestMouseDownInsideContainer
+ !this.isLatestMouseDownInsideContainer &&
+ this.isOpen()
) {
this.close();
}
diff --git a/src/app/pages/main/main.ts b/src/app/pages/main/main.ts
index 4bc55f7..a8f73e8 100644
--- a/src/app/pages/main/main.ts
+++ b/src/app/pages/main/main.ts
@@ -20,6 +20,40 @@ export class MainPage {
{ label: 'Deutsch', value: 'DE' },
{ label: 'Italiano', value: 'IT' },
{ label: '中文', value: 'CN' },
+ { label: 'Français', value: 'FR' },
+ { label: 'Español', value: 'ES' },
+ { label: 'Português', value: 'PT' },
+ { label: 'Русский', value: 'RU' },
+ { label: 'العربية', value: 'AR' },
+ { label: 'हिन्दी', value: 'HI' },
+ { label: 'বাংলা', value: 'BN' },
+ { label: 'Tiếng Việt', value: 'VI' },
+ { label: 'Türkçe', value: 'TR' },
+ { label: 'Polski', value: 'PL' },
+ { label: 'Українська', value: 'UK' },
+ { label: 'Nederlands', value: 'NL' },
+ { label: 'Ελληνικά', value: 'EL' },
+ { label: 'Magyar', value: 'HU' },
+ { label: 'Čeština', value: 'CS' },
+ { label: 'Română', value: 'RO' },
+ { label: 'Svenska', value: 'SV' },
+ { label: 'Dansk', value: 'DA' },
+ { label: 'Suomi', value: 'FI' },
+ { label: 'Norsk', value: 'NO' },
+ { label: 'ไทย', value: 'TH' },
+ { label: 'Bahasa Indonesia', value: 'ID' },
+ { label: 'Bahasa Melayu', value: 'MS' },
+ { label: 'Filipino', value: 'TL' },
+ { label: 'עברית', value: 'HE' },
+ { label: 'فارسی', value: 'FA' },
+ { label: 'اردو', value: 'UR' },
+ { label: 'தமிழ்', value: 'TA' },
+ { label: 'తెలుగు', value: 'TE' },
+ { label: 'ಕನ್ನಡ', value: 'KN' },
+ { label: 'മലയാളം', value: 'ML' },
+ { label: 'मराठी', value: 'MR' },
+ { label: 'ગુજરાતી', value: 'GU' },
+ { label: 'ਪੰਜਾਬੀ', value: 'PA' },
];
selectedDropdownValue = signal(null);