mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
♻️ refactor(shell-header): restructure with modular sub-components
Reorganize header into focused sub-components: - ShellNavigationToggleComponent: drawer toggle with dynamic icon - ShellFontSizeSelectorComponent: accessibility font size control - ShellLogoutButtonComponent: logout with AuthService integration - ShellNotificationsToggleComponent: notification panel overlay Add E2E testing attributes and ARIA accessibility support
This commit is contained in:
@@ -1 +1 @@
|
||||
export * from './lib/shell-header/shell-header.component';
|
||||
export * from './lib/shell-header.component';
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.selector {
|
||||
@apply relative flex rounded-full bg-isa-neutral-300;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
@apply absolute top-0 size-12 rounded-full bg-isa-neutral-700 transition-transform duration-200 ease-out;
|
||||
}
|
||||
|
||||
.option {
|
||||
@apply relative flex h-12 w-12 cursor-pointer items-center justify-center transition-colors duration-200;
|
||||
}
|
||||
|
||||
.option.selected {
|
||||
@apply text-isa-neutral-300;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<fieldset
|
||||
class="selector"
|
||||
data-what="fieldset"
|
||||
data-which="font-size-selector"
|
||||
role="radiogroup"
|
||||
aria-label="Schriftgröße auswählen"
|
||||
>
|
||||
<div class="indicator" [style.transform]="'translateX(' + indicatorOffset() + 'rem)'"></div>
|
||||
|
||||
@for (option of fontSizeOptions; track option.value) {
|
||||
<input
|
||||
type="radio"
|
||||
class="sr-only"
|
||||
[id]="'font-size-' + option.value"
|
||||
[value]="option.value"
|
||||
name="font-size"
|
||||
[ngModel]="fontSizeService.get()"
|
||||
(ngModelChange)="onFontSizeChange($event)"
|
||||
[attr.data-what]="'radio'"
|
||||
[attr.data-which]="'font-size-' + option.value"
|
||||
/>
|
||||
<label
|
||||
class="option"
|
||||
[for]="'font-size-' + option.value"
|
||||
[class.selected]="fontSizeService.get() === option.value"
|
||||
[attr.aria-label]="option.label"
|
||||
[attr.data-what]="'label'"
|
||||
[attr.data-which]="'font-size-label-' + option.value"
|
||||
>
|
||||
<ng-icon name="isaNavigationFontsize" [size]="option.iconSize" />
|
||||
</label>
|
||||
}
|
||||
</fieldset>
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { isaNavigationFontsize } from '@isa/icons';
|
||||
import { FontSize, FontSizeService } from '@isa/shell/common';
|
||||
|
||||
interface FontSizeOption {
|
||||
value: FontSize;
|
||||
label: string;
|
||||
iconSize: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'shell-font-size-selector',
|
||||
templateUrl: './font-size-selector.component.html',
|
||||
styleUrl: './font-size-selector.component.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [FormsModule, NgIcon],
|
||||
providers: [
|
||||
provideIcons({
|
||||
isaNavigationFontsize,
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class ShellFontSizeSelectorComponent {
|
||||
#logger = logger({ component: 'ShellFontSizeSelectorComponent' });
|
||||
|
||||
readonly fontSizeService = inject(FontSizeService);
|
||||
|
||||
readonly fontSizeOptions: FontSizeOption[] = [
|
||||
{ value: 'small', label: 'Kleine Schriftgröße', iconSize: '0.63rem' },
|
||||
{ value: 'medium', label: 'Mittlere Schriftgröße', iconSize: '1rem' },
|
||||
{ value: 'large', label: 'Große Schriftgröße', iconSize: '1.3rem' },
|
||||
];
|
||||
|
||||
readonly #offsetMap: Record<FontSize, number> = {
|
||||
small: 0,
|
||||
medium: 3,
|
||||
large: 6,
|
||||
};
|
||||
|
||||
readonly indicatorOffset = computed(
|
||||
() => this.#offsetMap[this.fontSizeService.get()],
|
||||
);
|
||||
|
||||
onFontSizeChange(size: FontSize): void {
|
||||
this.#logger.debug('Font size changed', () => ({ size }));
|
||||
this.fontSizeService.set(size);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { InfoButtonComponent } from '@isa/ui/buttons';
|
||||
import { AuthService } from '@isa/core/auth';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { isaNavigationLogout } from '@isa/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-logout-button',
|
||||
template: `<ui-info-button
|
||||
(click)="logout()"
|
||||
data-what="button"
|
||||
data-which="logout-button"
|
||||
aria-label="Abmelden"
|
||||
>
|
||||
<ng-icon uiInfoButtonIcon name="isaNavigationLogout" />
|
||||
<span class="isa-text-body-2-bold" uiInfoButtonLabel>NEU</span>
|
||||
</ui-info-button>`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [InfoButtonComponent, NgIcon],
|
||||
providers: [provideIcons({ isaNavigationLogout })],
|
||||
})
|
||||
export class ShellLogoutButtonComponent {
|
||||
#logger = logger({ component: 'ShellLogoutButtonComponent' });
|
||||
#authService = inject(AuthService);
|
||||
|
||||
logout(): void {
|
||||
this.#logger.info('User logging out');
|
||||
this.#authService.logout();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { isaActionClose, isaNavigationSidemenu } from '@isa/icons';
|
||||
import { NavigationService } from '@isa/shell/common';
|
||||
import {
|
||||
IconButtonComponent,
|
||||
IconButtonColor,
|
||||
IconButtonSize,
|
||||
} from '@isa/ui/buttons';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-navigation-toggle',
|
||||
template: `<ui-icon-button
|
||||
[name]="iconName()"
|
||||
[color]="IconButtonColor.Primary"
|
||||
[size]="IconButtonSize.Large"
|
||||
(click)="toggle()"
|
||||
data-what="button"
|
||||
data-which="navigation-toggle"
|
||||
[attr.aria-label]="ariaLabel()"
|
||||
[attr.aria-expanded]="navigationService.get()"
|
||||
/>`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [IconButtonComponent],
|
||||
providers: [provideIcons({ isaNavigationSidemenu, isaActionClose })],
|
||||
})
|
||||
export class ShellNavigationToggleComponent {
|
||||
#logger = logger({ component: 'ShellNavigationToggleComponent' });
|
||||
|
||||
readonly navigationService = inject(NavigationService);
|
||||
|
||||
readonly IconButtonColor = IconButtonColor;
|
||||
readonly IconButtonSize = IconButtonSize;
|
||||
|
||||
iconName = computed(() => {
|
||||
const open = this.navigationService.get();
|
||||
return open ? 'isaActionClose' : 'isaNavigationSidemenu';
|
||||
});
|
||||
|
||||
ariaLabel = computed(() =>
|
||||
this.navigationService.get() ? 'Menü schließen' : 'Menü öffnen',
|
||||
);
|
||||
|
||||
toggle(): void {
|
||||
this.navigationService.toggle();
|
||||
this.#logger.debug('Navigation toggled', () => ({
|
||||
isOpen: this.navigationService.get(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<ui-icon-button
|
||||
cdkOverlayOrigin
|
||||
#trigger="cdkOverlayOrigin"
|
||||
[name]="icon()"
|
||||
[color]="IconButtonColor.Tertiary"
|
||||
[size]="IconButtonSize.Large"
|
||||
[disabled]="!hasNotifications()"
|
||||
(click)="toggle()"
|
||||
data-what="button"
|
||||
data-which="notifications-toggle"
|
||||
[attr.aria-label]="ariaLabel()"
|
||||
[attr.aria-expanded]="isOpen()"
|
||||
aria-haspopup="true"
|
||||
/>
|
||||
|
||||
<ng-template
|
||||
cdkConnectedOverlay
|
||||
[cdkConnectedOverlayOrigin]="trigger"
|
||||
[cdkConnectedOverlayOpen]="isOpen()"
|
||||
[cdkConnectedOverlayPositions]="positions"
|
||||
[cdkConnectedOverlayOffsetY]="12"
|
||||
[cdkConnectedOverlayHasBackdrop]="true"
|
||||
cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop"
|
||||
(backdropClick)="close()"
|
||||
(detach)="close()"
|
||||
>
|
||||
<div
|
||||
class="p-4 bg-isa-white rounded-2xl max-h-96 overflow-y-auto shadow-lg"
|
||||
data-what="panel"
|
||||
data-which="notifications-panel"
|
||||
role="dialog"
|
||||
aria-label="Benachrichtigungen"
|
||||
>
|
||||
<shell-notifications />
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedPosition } from '@angular/cdk/overlay';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { isaNavigationMessage, isaNavigationMessageUnread } from '@isa/icons';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import {
|
||||
IconButtonComponent,
|
||||
IconButtonColor,
|
||||
IconButtonSize,
|
||||
} from '@isa/ui/buttons';
|
||||
import { NotificationsService } from '@isa/shell/common';
|
||||
import { ShellNotificationsComponent } from '@isa/shell/notifications';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-notifications-toggle',
|
||||
templateUrl: './notifications-toggle.component.html',
|
||||
styleUrl: './notifications-toggle.component.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [IconButtonComponent, ShellNotificationsComponent, CdkOverlayOrigin, CdkConnectedOverlay],
|
||||
providers: [
|
||||
provideIcons({ isaNavigationMessage, isaNavigationMessageUnread }),
|
||||
],
|
||||
})
|
||||
export class ShellNotificationsToggleComponent {
|
||||
#logger = logger({ component: 'ShellNotificationsToggleComponent' });
|
||||
|
||||
readonly IconButtonColor = IconButtonColor;
|
||||
readonly IconButtonSize = IconButtonSize;
|
||||
|
||||
readonly notificationsService = inject(NotificationsService);
|
||||
|
||||
isOpen = signal(false);
|
||||
|
||||
readonly positions: ConnectedPosition[] = [
|
||||
// Bottom right
|
||||
{ originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top' },
|
||||
// Bottom left
|
||||
{ originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top' },
|
||||
// Left top
|
||||
{ originX: 'start', originY: 'top', overlayX: 'end', overlayY: 'top' },
|
||||
// Left bottom
|
||||
{ originX: 'start', originY: 'bottom', overlayX: 'end', overlayY: 'bottom' },
|
||||
];
|
||||
|
||||
toggle(): void {
|
||||
this.isOpen.update((open) => !open);
|
||||
this.#logger.debug('Notifications panel toggled', () => ({
|
||||
isOpen: this.isOpen(),
|
||||
}));
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.isOpen.set(false);
|
||||
this.#logger.debug('Notifications panel closed');
|
||||
}
|
||||
|
||||
hasNotifications = computed(() => this.notificationsService.get().length > 0);
|
||||
|
||||
unreadNotifications = computed(
|
||||
() => this.notificationsService.unreadCount() > 0,
|
||||
);
|
||||
|
||||
icon = computed(() =>
|
||||
this.unreadNotifications()
|
||||
? 'isaNavigationMessageUnread'
|
||||
: 'isaNavigationMessage',
|
||||
);
|
||||
|
||||
ariaLabel = computed(() => {
|
||||
if (!this.hasNotifications()) return 'Keine Benachrichtigungen';
|
||||
return this.isOpen() ? 'Benachrichtigungen schließen' : 'Benachrichtigungen öffnen';
|
||||
});
|
||||
}
|
||||
14
libs/shell/header/src/lib/shell-header.component.html
Normal file
14
libs/shell/header/src/lib/shell-header.component.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<header
|
||||
class="flex items-center gap-2 px-4 desktop:px-6 py-2 bg-isa-white shadow-[0_2px_6px_0_rgba(0,0,0,0.1)]"
|
||||
data-what="header"
|
||||
data-which="shell-header"
|
||||
role="banner"
|
||||
>
|
||||
@if (isTablet()) {
|
||||
<shell-navigation-toggle />
|
||||
}
|
||||
<div class="grow"></div>
|
||||
<shell-font-size-selector />
|
||||
<shell-logout-button />
|
||||
<shell-notifications-toggle />
|
||||
</header>
|
||||
22
libs/shell/header/src/lib/shell-header.component.ts
Normal file
22
libs/shell/header/src/lib/shell-header.component.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { ShellNavigationToggleComponent } from './components/navigation-toggle.component';
|
||||
import { Breakpoint, breakpoint } from '@isa/ui/layout';
|
||||
import { ShellFontSizeSelectorComponent } from './components/font-size-selector/font-size-selector.component';
|
||||
import { ShellLogoutButtonComponent } from './components/logout-button.component';
|
||||
import { ShellNotificationsToggleComponent } from './components/notifications-toggle/notifications-toggle.component';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-header',
|
||||
templateUrl: './shell-header.component.html',
|
||||
styleUrls: ['./shell-header.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
ShellNavigationToggleComponent,
|
||||
ShellFontSizeSelectorComponent,
|
||||
ShellLogoutButtonComponent,
|
||||
ShellNotificationsToggleComponent,
|
||||
],
|
||||
})
|
||||
export class ShellHeaderComponent {
|
||||
readonly isTablet = breakpoint(Breakpoint.Tablet);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<p>ShellHeader works!</p>
|
||||
@@ -1,21 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ShellHeaderComponent } from './shell-header.component';
|
||||
|
||||
describe('ShellHeaderComponent', () => {
|
||||
let component: ShellHeaderComponent;
|
||||
let fixture: ComponentFixture<ShellHeaderComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ShellHeaderComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ShellHeaderComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-shell-header',
|
||||
imports: [CommonModule],
|
||||
templateUrl: './shell-header.component.html',
|
||||
styleUrl: './shell-header.component.css',
|
||||
})
|
||||
export class ShellHeaderComponent {}
|
||||
Reference in New Issue
Block a user