feat(a11y): use native dialog for language selection

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fraujulianandClaude Opus 5.5 committed 2026-10-15 16:23:41 +02:00
1 parent 9ede5c02c1
commit 20aaf4faa7
3 files changed
+40 -36

No files matched your search

+6 -6
View File
@@ -1,4 +1,4 @@
<div class="app-content" [attr.aria-hidden]="isLanguageSelectorOpen() ? 'true' : null" [attr.inert]="isLanguageSelectorOpen() ? '' : null">
<div class="app-content">
<router-outlet></router-outlet>
</div>
@@ -16,14 +16,14 @@
</span>
</button>
@if (isLanguageSelectorOpen()) {
<div
<dialog
#languageDialog
class="language-overlay"
role="dialog"
aria-modal="true"
aria-labelledby="language-title"
(close)="closeLanguageSelector()"
(click)="closeLanguageSelectorOnBackdrop($event)"
>
@if (isLanguageSelectorOpen()) {
<section class="language-card panel">
<div class="language-card-header">
<div class="language-copy">
@@ -57,5 +57,5 @@
}
</div>
</section>
</div>
}
</dialog>
+15 -5
View File
@@ -27,7 +27,7 @@ describe('AppComponent', (): void => {
it('should keep the language selector closed on first visit', (): void => {
const el = fixture.nativeElement as HTMLElement;
expect(el.querySelector('.language-overlay')).toBeNull();
expect(el.querySelector<HTMLDialogElement>('dialog.language-overlay')?.open).toBe(false);
});
it('should preselect English by default', (): void => {
@@ -51,7 +51,6 @@ describe('AppComponent', (): void => {
const comp = component as unknown as {
chooseLanguage(language: 'en' | 'de'): void;
currentLanguageCode(): string;
isLanguageConfirmed(): boolean;
isLanguageSelectorOpen(): boolean;
};
@@ -59,7 +58,6 @@ describe('AppComponent', (): void => {
fixture.detectChanges();
expect(comp.currentLanguageCode()).toBe('de');
expect(comp.isLanguageConfirmed()).toBe(true);
expect(comp.isLanguageSelectorOpen()).toBe(false);
expect(localStorage.getItem('portfolio-language')).toBe('de');
});
@@ -71,12 +69,10 @@ describe('AppComponent', (): void => {
restoredFixture.detectChanges();
const restoredComponent = restoredFixture.componentInstance as unknown as {
currentLanguageCode(): string;
isLanguageConfirmed(): boolean;
isLanguageSelectorOpen(): boolean;
};
expect(restoredComponent.currentLanguageCode()).toBe('de');
expect(restoredComponent.isLanguageConfirmed()).toBe(true);
expect(restoredComponent.isLanguageSelectorOpen()).toBe(false);
});
@@ -93,4 +89,18 @@ describe('AppComponent', (): void => {
expect(comp.isLanguageSelectorOpen()).toBe(true);
});
it('should sync the selector state when the dialog closes natively', (): void => {
const comp = component as unknown as {
reopenLanguageSelector(): void;
isLanguageSelectorOpen(): boolean;
};
comp.reopenLanguageSelector();
fixture.detectChanges();
const dialog = (fixture.nativeElement as HTMLElement).querySelector('dialog');
dialog?.dispatchEvent(new Event('close'));
expect(comp.isLanguageSelectorOpen()).toBe(false);
});
});
+19 -25
View File
@@ -1,17 +1,18 @@
import { DOCUMENT } from '@angular/common';
import {
afterRenderEffect,
ChangeDetectionStrategy,
Component,
computed,
effect,
inject,
signal,
viewChild,
} from '@angular/core';
import type { OnDestroy, OnInit } from '@angular/core';
import type { ElementRef, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import type { LanguageCode } from '../languages/language.types';
import type { LanguageOption } from './app.types';
import { LanguagePreferenceService } from './services/language-preference.service';
import { ShellLanguageService } from './services/shell-language.service';
@Component({
@@ -21,13 +22,14 @@ import { ShellLanguageService } from './services/shell-language.service';
templateUrl: './app.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements OnInit, OnDestroy {
private readonly document = inject(DOCUMENT);
export class AppComponent implements OnInit {
private readonly languagePreferenceService = inject(LanguagePreferenceService);
private readonly shellLanguageService = inject(ShellLanguageService);
private readonly languageDialog =
viewChild.required<ElementRef<HTMLDialogElement>>('languageDialog');
protected readonly content = this.shellLanguageService.content;
protected readonly currentLanguageCode = this.shellLanguageService.languageCode;
protected readonly isLanguageConfirmed = this.shellLanguageService.isLanguageConfirmed;
protected readonly currentLanguageCode = this.languagePreferenceService.languageCode;
protected readonly isLanguageSelectorOpen = signal(false);
protected readonly selectedLanguage = signal<LanguageCode>('en');
protected readonly languageOptions: readonly LanguageOption[] = [
@@ -41,26 +43,22 @@ export class AppComponent implements OnInit, OnDestroy {
this.getLanguageLabel(this.currentLanguageCode(), this.content()),
);
constructor() {
effect((): void => {
const overflowValue = this.isLanguageSelectorOpen() ? 'hidden' : '';
this.document.body.style.overflow = overflowValue;
this.document.documentElement.style.overflow = overflowValue;
});
private readonly syncDialog = afterRenderEffect((): void => {
const dialog = this.languageDialog().nativeElement;
if (this.isLanguageSelectorOpen() && !dialog.open) {
dialog.showModal();
} else if (!this.isLanguageSelectorOpen() && dialog.open) {
dialog.close();
}
});
ngOnInit(): void {
const initialLanguage = this.shellLanguageService.initializeFromStorage();
this.selectedLanguage.set(initialLanguage);
this.selectedLanguage.set(this.languagePreferenceService.initialize());
}
protected chooseLanguage(language: LanguageCode): void {
this.selectedLanguage.set(language);
this.confirmLanguage();
}
protected confirmLanguage(): void {
this.shellLanguageService.confirmLanguage(this.selectedLanguage());
this.languagePreferenceService.confirmLanguage(language);
this.isLanguageSelectorOpen.set(false);
}
@@ -69,6 +67,7 @@ export class AppComponent implements OnInit, OnDestroy {
this.isLanguageSelectorOpen.set(true);
}
/** Also runs when the dialog closes natively, e.g. via Escape. */
protected closeLanguageSelector(): void {
this.selectedLanguage.set(this.currentLanguageCode());
this.isLanguageSelectorOpen.set(false);
@@ -83,9 +82,4 @@ export class AppComponent implements OnInit, OnDestroy {
protected getLanguageLabel(language: LanguageCode, content = this.dialogContent()): string {
return language === 'de' ? content.app.languageGerman : content.app.languageEnglish;
}
ngOnDestroy(): void {
this.document.body.style.overflow = '';
this.document.documentElement.style.overflow = '';
}
}