Skip to content
Snippets Groups Projects
Commit e9adba43 authored by OZGCloud's avatar OZGCloud
Browse files

Merge pull request 'OZG-3539-mail-notifications' (#772) from...

Merge pull request 'OZG-3539-mail-notifications' (#772) from OZG-3539-mail-notifications into master

Reviewed-on: https://git.ozg-sh.de/ozgcloud-app/alfa/pulls/772


Reviewed-by: default avatarOZGCloud <ozgcloud@mgm-tp.com>
parents d4897ca0 e94b6ab9
Branches
Tags
No related merge requests found
Showing
with 280 additions and 104 deletions
...@@ -15,13 +15,13 @@ import { StoreModule } from '@ngrx/store'; ...@@ -15,13 +15,13 @@ import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools'; import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { import {
AdminLogoIconComponent, AdminLogoIconComponent,
DropdownMenuButtonComponent,
DropdownMenuComponent,
LogoutIconComponent, LogoutIconComponent,
MailboxIconComponent, MailboxIconComponent,
NavItemComponent, NavItemComponent,
NavbarComponent, NavbarComponent,
OrgaUnitIconComponent, OrgaUnitIconComponent,
PopupComponent,
PopupListItemComponent,
} from '@ods/system'; } from '@ods/system';
import { OAuthModule } from 'angular-oauth2-oidc'; import { OAuthModule } from 'angular-oauth2-oidc';
import { HttpUnauthorizedInterceptor } from 'libs/authentication/src/lib/http-unauthorized.interceptor'; import { HttpUnauthorizedInterceptor } from 'libs/authentication/src/lib/http-unauthorized.interceptor';
...@@ -44,8 +44,8 @@ import { appRoutes } from './app.routes'; ...@@ -44,8 +44,8 @@ import { appRoutes } from './app.routes';
imports: [ imports: [
CommonModule, CommonModule,
AdminLogoIconComponent, AdminLogoIconComponent,
PopupComponent, DropdownMenuComponent,
PopupListItemComponent, DropdownMenuButtonComponent,
NavItemComponent, NavItemComponent,
NavbarComponent, NavbarComponent,
OrgaUnitIconComponent, OrgaUnitIconComponent,
......
<ods-popup buttonClass="rounded-full"> <ods-dropdown-menu buttonClass="rounded-full">
<div <div
button-content button-content
role="img" role="img"
...@@ -8,11 +8,11 @@ ...@@ -8,11 +8,11 @@
{{ currentUserInitials }} {{ currentUserInitials }}
</p> </p>
</div> </div>
<ods-popup-list-item <ods-dropdown-menu-button
caption="Abmelden" caption="Abmelden"
(itemClicked)="authenticationService.logout()" (itemClicked)="authenticationService.logout()"
data-test-id="popup-logout-button" data-test-id="popup-logout-button"
> >
<ods-logout-icon icon /> <ods-logout-icon icon />
</ods-popup-list-item> </ods-dropdown-menu-button>
</ods-popup> </ods-dropdown-menu>
...@@ -6,7 +6,11 @@ import { ...@@ -6,7 +6,11 @@ import {
} from '@alfa-client/test-utils'; } from '@alfa-client/test-utils';
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { LogoutIconComponent, PopupComponent, PopupListItemComponent } from '@ods/system'; import {
DropdownMenuButtonComponent,
DropdownMenuComponent,
LogoutIconComponent,
} from '@ods/system';
import { AuthenticationService } from 'authentication'; import { AuthenticationService } from 'authentication';
import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test';
import { MockComponent } from 'ng-mocks'; import { MockComponent } from 'ng-mocks';
...@@ -26,8 +30,8 @@ describe('UserProfileButtonContainerComponent', () => { ...@@ -26,8 +30,8 @@ describe('UserProfileButtonContainerComponent', () => {
declarations: [UserProfileButtonContainerComponent], declarations: [UserProfileButtonContainerComponent],
imports: [ imports: [
RouterTestingModule, RouterTestingModule,
MockComponent(PopupComponent), MockComponent(DropdownMenuComponent),
MockComponent(PopupListItemComponent), MockComponent(DropdownMenuButtonComponent),
MockComponent(LogoutIconComponent), MockComponent(LogoutIconComponent),
], ],
providers: [ providers: [
......
...@@ -25,16 +25,32 @@ import { TOGGLE_ELEMENT } from '../../support/angular.util'; ...@@ -25,16 +25,32 @@ import { TOGGLE_ELEMENT } from '../../support/angular.util';
export class UserSettingsE2EComponent { export class UserSettingsE2EComponent {
private readonly rootLocator: string = 'user-settings'; private readonly rootLocator: string = 'user-settings';
private readonly emailBenachrichtigungLocator: string = 'email-benachrichtigung'; private readonly emailBenachrichtigungNewVorgangLocator: string = 'email-benachrichtigung-neuer-Vorgang';
private readonly emailBenachrichtigungVorgangAssignedToUserLocator: string = 'email-benachrichtigung-neuer-Vorgang-Assigned';
private readonly emailBenachrichtigungPostfachNachrichtFromAntragsteller: string =
'email-benachrichtigung-neue-Nachricht-Antragsteller';
private readonly emailBenachrichtigungWiedervorlageDueToday: string = 'email-benachrichtigung-Faellige-Wiedervorlage';
private readonly darkModeLocator: string = 'dark-mode'; private readonly darkModeLocator: string = 'dark-mode';
private readonly buttonLocator: string = 'icon-button'; private readonly buttonLocator: string = 'user-settings-button';
public getRoot() { public getRoot() {
return cy.getTestElementWithOid(this.rootLocator); return cy.getTestElementWithOid(this.rootLocator);
} }
public getEmailBenachrichtigung(): ToggleE2EComponent { public getEmailBenachrichtigungForNewVorgang(): ToggleE2EComponent {
return new ToggleE2EComponent(this.emailBenachrichtigungLocator); return new ToggleE2EComponent(this.emailBenachrichtigungNewVorgangLocator);
}
public getEmailBenachrichtigungForVorgangAssignedToUser(): ToggleE2EComponent {
return new ToggleE2EComponent(this.emailBenachrichtigungVorgangAssignedToUserLocator);
}
public getEmailBenachrichtigungForPostfachNachrichtFromAntragsteller(): ToggleE2EComponent {
return new ToggleE2EComponent(this.emailBenachrichtigungPostfachNachrichtFromAntragsteller);
}
public getEmailBenachrichtigungForWiedervorlageDueToday(): ToggleE2EComponent {
return new ToggleE2EComponent(this.emailBenachrichtigungWiedervorlageDueToday);
} }
public getDarkMode(): ToggleE2EComponent { public getDarkMode(): ToggleE2EComponent {
...@@ -42,7 +58,7 @@ export class UserSettingsE2EComponent { ...@@ -42,7 +58,7 @@ export class UserSettingsE2EComponent {
} }
public getButton() { public getButton() {
return this.getRoot().findTestElementWithClass(this.buttonLocator); return this.getRoot().getTestElementWithOid(this.buttonLocator);
} }
} }
......
...@@ -4,7 +4,7 @@ import { VorgangSearchE2EComponent } from 'apps/alfa-e2e/src/components/vorgang/ ...@@ -4,7 +4,7 @@ import { VorgangSearchE2EComponent } from 'apps/alfa-e2e/src/components/vorgang/
import { VorgangViewsE2EComponent } from 'apps/alfa-e2e/src/components/vorgang/vorgang-views.e2e.component'; import { VorgangViewsE2EComponent } from 'apps/alfa-e2e/src/components/vorgang/vorgang-views.e2e.component';
import { HeaderE2EComponent } from 'apps/alfa-e2e/src/page-objects/header.po'; import { HeaderE2EComponent } from 'apps/alfa-e2e/src/page-objects/header.po';
import { MainPage, waitForSpinnerToDisappear } from 'apps/alfa-e2e/src/page-objects/main.po'; import { MainPage, waitForSpinnerToDisappear } from 'apps/alfa-e2e/src/page-objects/main.po';
import { isKeyboardFocused } from 'apps/alfa-e2e/src/support/angular.util'; import { isKeyboardFocused, isOdsFocused } from 'apps/alfa-e2e/src/support/angular.util';
import { dropCollections, pressTab } from 'apps/alfa-e2e/src/support/cypress-helper'; import { dropCollections, pressTab } from 'apps/alfa-e2e/src/support/cypress-helper';
import { exist, haveFocus } from 'apps/alfa-e2e/src/support/cypress.util'; import { exist, haveFocus } from 'apps/alfa-e2e/src/support/cypress.util';
import { initUsermanagerUsers, loginAsSabine } from 'apps/alfa-e2e/src/support/user-util'; import { initUsermanagerUsers, loginAsSabine } from 'apps/alfa-e2e/src/support/user-util';
...@@ -63,7 +63,7 @@ describe('VorgangList Page', () => { ...@@ -63,7 +63,7 @@ describe('VorgangList Page', () => {
it('should focus settings icon', () => { it('should focus settings icon', () => {
pressTab(); pressTab();
isKeyboardFocused(header.getUserSettings().getButton()); isOdsFocused(header.getUserSettings().getButton());
}); });
it('should focus user icon', () => { it('should focus user icon', () => {
......
...@@ -58,8 +58,20 @@ describe('User Settings', () => { ...@@ -58,8 +58,20 @@ describe('User Settings', () => {
userSettings.getRoot().click(); userSettings.getRoot().click();
}); });
it('should show notificationsSendsFor toggle', () => { it('should show benachrichtigung neuer Vorgang toggle', () => {
exist(userSettings.getEmailBenachrichtigung().getRoot()); exist(userSettings.getEmailBenachrichtigungForNewVorgang().getRoot());
});
it('should show benachrichtigung neue nachricht antragsteller toggle', () => {
exist(userSettings.getEmailBenachrichtigungForPostfachNachrichtFromAntragsteller().getRoot());
});
it('should show benachrichtigung vorgang mir zugewiesen toggle', () => {
exist(userSettings.getEmailBenachrichtigungForVorgangAssignedToUser().getRoot());
});
it('should show benachrichtigung faellige wiedervorlage toggle', () => {
exist(userSettings.getEmailBenachrichtigungForWiedervorlageDueToday().getRoot());
}); });
it('should show darkMode toggle', () => { it('should show darkMode toggle', () => {
...@@ -67,22 +79,79 @@ describe('User Settings', () => { ...@@ -67,22 +79,79 @@ describe('User Settings', () => {
}); });
}); });
describe('click on notificationSendsFor toggle', () => { describe('click on neuer Vorgang toggle', () => {
it('should have initial unchecked toggle', () => {
isNotChecked(userSettings.getEmailBenachrichtigungForNewVorgang().getToggle());
});
it('should switch toggle status', () => {
userSettings.getEmailBenachrichtigungForNewVorgang().getToggle().click();
isChecked(userSettings.getEmailBenachrichtigungForNewVorgang().getToggle());
});
it('should be loaded after page reload', () => {
reload();
userSettings.getRoot().click();
isChecked(userSettings.getEmailBenachrichtigungForNewVorgang().getToggle());
});
});
describe('click on neue nachricht antragsteller toggle', () => {
it('should have initial unchecked toggle', () => {
isNotChecked(userSettings.getEmailBenachrichtigungForPostfachNachrichtFromAntragsteller().getToggle());
});
it('should switch toggle status', () => {
userSettings.getEmailBenachrichtigungForPostfachNachrichtFromAntragsteller().getToggle().click();
isChecked(userSettings.getEmailBenachrichtigungForPostfachNachrichtFromAntragsteller().getToggle());
});
it('should be loaded after page reload', () => {
reload();
userSettings.getRoot().click();
isChecked(userSettings.getEmailBenachrichtigungForPostfachNachrichtFromAntragsteller().getToggle());
});
});
describe('click on vorgang mir zugewiesen toggle', () => {
it('should have initial unchecked toggle', () => {
isNotChecked(userSettings.getEmailBenachrichtigungForVorgangAssignedToUser().getToggle());
});
it('should switch toggle status', () => {
userSettings.getEmailBenachrichtigungForVorgangAssignedToUser().getToggle().click();
isChecked(userSettings.getEmailBenachrichtigungForVorgangAssignedToUser().getToggle());
});
it('should be loaded after page reload', () => {
reload();
userSettings.getRoot().click();
isChecked(userSettings.getEmailBenachrichtigungForVorgangAssignedToUser().getToggle());
});
});
describe('click on faellige wiedervorlage toggle', () => {
it('should have initial unchecked toggle', () => { it('should have initial unchecked toggle', () => {
isNotChecked(userSettings.getEmailBenachrichtigung().getToggle()); isNotChecked(userSettings.getEmailBenachrichtigungForWiedervorlageDueToday().getToggle());
}); });
it('should switch toggle status', () => { it('should switch toggle status', () => {
userSettings.getEmailBenachrichtigung().getToggle().click(); userSettings.getEmailBenachrichtigungForWiedervorlageDueToday().getToggle().click();
isChecked(userSettings.getEmailBenachrichtigung().getToggle()); isChecked(userSettings.getEmailBenachrichtigungForWiedervorlageDueToday().getToggle());
}); });
it('should be loaded after page reload', () => { it('should be loaded after page reload', () => {
reload(); reload();
userSettings.getRoot().click(); userSettings.getRoot().click();
isChecked(userSettings.getEmailBenachrichtigung().getToggle()); isChecked(userSettings.getEmailBenachrichtigungForWiedervorlageDueToday().getToggle());
}); });
}); });
......
{ {
"_id": { "_id": {
"$oid": "645e6fa20cfafc0fbbe6bf73" "$oid": "66fbace8d001317c611681ed"
}, },
"createdAt": { "createdAt": {
"$date": "2024-09-03T10:25:17.001Z" "$date": "2024-09-03T10:25:17.001Z"
}, },
"deleted": false, "deleted": false,
"keycloakUserId": "2ccf0c13-da74-4516-ae3d-f46d30e8ec0c", "keycloakUserId": "625718b7-61ea-43e9-841b-0b01438f6cbc",
"firstName": "Ariane", "firstName": "Ariane",
"fullName": "Ariane Admin", "fullName": "Ariane Admin",
"lastName": "Admin", "lastName": "Admin",
......
{ {
"_id": { "_id": {
"$oid": "63284e55c39b316b2ad02e2c" "$oid": "66fbb761bdbeecbd2b1681ed"
}, },
"createdAt": { "createdAt": {
"$date": "2022-02-18T09:21:24.340Z" "$date": "2022-02-18T09:21:24.340Z"
......
{ {
"_id": { "_id": {
"$oid": "645e6fa20cfafc0fbbe6bf73" "$oid": "66fbacfdd001317c611681ee"
}, },
"createdAt": { "createdAt": {
"$date": "2024-08-14T13:11:56.489Z" "$date": "2024-08-14T13:11:56.489Z"
}, },
"deleted": false, "deleted": false,
"keycloakUserId": "2ccf0c13-da74-4516-ae3d-f46d30e8ec0c", "keycloakUserId": "2320657a-dc97-4b9f-ae24-abec842e23ef",
"firstName": "Zelda", "firstName": "Zelda",
"fullName": "Zelda Zusammen", "fullName": "Zelda Zusammen",
"lastName": "Zusammen", "lastName": "Zusammen",
......
...@@ -30,6 +30,7 @@ enum AngularClassesE2E { ...@@ -30,6 +30,7 @@ enum AngularClassesE2E {
MAT_BUTTONG_TOGGLE_CHECKED = 'mat-button-toggle-checked', MAT_BUTTONG_TOGGLE_CHECKED = 'mat-button-toggle-checked',
MAT_FOCUSED = 'mat-focused', MAT_FOCUSED = 'mat-focused',
CDK_KEYBOARD_FOCUSED = 'cdk-keyboard-focused', CDK_KEYBOARD_FOCUSED = 'cdk-keyboard-focused',
ODS_FOCUSED = 'ods-focused',
MAT_BADGE_HIDDEN = 'mat-badge-hidden', MAT_BADGE_HIDDEN = 'mat-badge-hidden',
} }
...@@ -71,6 +72,10 @@ export function isKeyboardFocused(element: any) { ...@@ -71,6 +72,10 @@ export function isKeyboardFocused(element: any) {
containClass(element, AngularClassesE2E.CDK_KEYBOARD_FOCUSED); containClass(element, AngularClassesE2E.CDK_KEYBOARD_FOCUSED);
} }
export function isOdsFocused(element: any) {
containClass(element, AngularClassesE2E.ODS_FOCUSED);
}
export function expectIconWithoutBadge(element: any): void { export function expectIconWithoutBadge(element: any): void {
containClass(element, AngularClassesE2E.MAT_BADGE_HIDDEN); containClass(element, AngularClassesE2E.MAT_BADGE_HIDDEN);
} }
......
...@@ -5,6 +5,8 @@ export * from './lib/bescheid-status-text/bescheid-status-text.component'; ...@@ -5,6 +5,8 @@ export * from './lib/bescheid-status-text/bescheid-status-text.component';
export * from './lib/bescheid-wrapper/bescheid-wrapper.component'; export * from './lib/bescheid-wrapper/bescheid-wrapper.component';
export * from './lib/button-card/button-card.component'; export * from './lib/button-card/button-card.component';
export * from './lib/button/button.component'; export * from './lib/button/button.component';
export * from './lib/dropdown-menu/dropdown-menu-button/dropdown-menu-button.component';
export * from './lib/dropdown-menu/dropdown-menu/dropdown-menu.component';
export * from './lib/form/checkbox/checkbox.component'; export * from './lib/form/checkbox/checkbox.component';
export * from './lib/form/error-message/error-message.component'; export * from './lib/form/error-message/error-message.component';
export * from './lib/form/fieldset/fieldset.component'; export * from './lib/form/fieldset/fieldset.component';
...@@ -34,6 +36,4 @@ export * from './lib/instant-search/instant-search/instant-search.component'; ...@@ -34,6 +36,4 @@ export * from './lib/instant-search/instant-search/instant-search.component';
export * from './lib/instant-search/instant-search/instant-search.model'; export * from './lib/instant-search/instant-search/instant-search.model';
export * from './lib/navbar/nav-item/nav-item.component'; export * from './lib/navbar/nav-item/nav-item.component';
export * from './lib/navbar/navbar/navbar.component'; export * from './lib/navbar/navbar/navbar.component';
export * from './lib/popup/popup-list-item/popup-list-item.component';
export * from './lib/popup/popup/popup.component';
export * from './lib/testbtn/testbtn.component'; export * from './lib/testbtn/testbtn.component';
import { dispatchEventFromFixture } from '@alfa-client/test-utils'; import { dispatchEventFromFixture } from '@alfa-client/test-utils';
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PopupListItemComponent } from './popup-list-item.component'; import { DropdownMenuButtonComponent } from './dropdown-menu-button.component';
describe('PopupListItemComponent', () => { describe('DropdownMenuButtonComponent', () => {
let component: PopupListItemComponent; let component: DropdownMenuButtonComponent;
let fixture: ComponentFixture<PopupListItemComponent>; let fixture: ComponentFixture<DropdownMenuButtonComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [PopupListItemComponent], imports: [DropdownMenuButtonComponent],
}).compileComponents(); }).compileComponents();
fixture = TestBed.createComponent(PopupListItemComponent); fixture = TestBed.createComponent(DropdownMenuButtonComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
......
...@@ -2,19 +2,20 @@ import { CommonModule } from '@angular/common'; ...@@ -2,19 +2,20 @@ import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({ @Component({
selector: 'ods-popup-list-item', selector: 'ods-dropdown-menu-button',
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule],
template: `<button template: `<button
class="flex min-h-12 w-full items-center gap-4 border-2 border-transparent bg-whitetext px-4 py-3 text-start outline-none hover:border-primary focus-visible:border-focus" class="flex min-h-12 w-full items-center gap-4 border-2 border-transparent px-4 py-3
role="listitem" text-start outline-none hover:border-primary focus-visible:border-focus"
role="menuitem"
(click)="itemClicked.emit()" (click)="itemClicked.emit()"
> >
<ng-content select="[icon]" /> <ng-content select="[icon]" />
<p class="text-text">{{ caption }}</p> <p class="text-text">{{ caption }}</p>
</button>`, </button>`,
}) })
export class PopupListItemComponent { export class DropdownMenuButtonComponent {
@Input({ required: true }) caption!: string; @Input({ required: true }) caption!: string;
@Output() itemClicked: EventEmitter<MouseEvent> = new EventEmitter(); @Output() itemClicked: EventEmitter<MouseEvent> = new EventEmitter();
......
import { getElementFromFixture } from '@alfa-client/test-utils'; import { getElementFromFixture } from '@alfa-client/test-utils';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test';
import { PopupComponent } from './popup.component'; import { DropdownMenuComponent } from './dropdown-menu.component';
describe('PopupComponent', () => { describe('DropdownMenuComponent', () => {
let component: PopupComponent; let component: DropdownMenuComponent;
let fixture: ComponentFixture<PopupComponent>; let fixture: ComponentFixture<DropdownMenuComponent>;
const popupButton: string = getDataTestIdOf('popup-button'); const dropdownButton: string = getDataTestIdOf('dropdown-button');
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [PopupComponent], imports: [DropdownMenuComponent],
}).compileComponents(); }).compileComponents();
fixture = TestBed.createComponent(PopupComponent); fixture = TestBed.createComponent(DropdownMenuComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
...@@ -40,6 +40,24 @@ describe('PopupComponent', () => { ...@@ -40,6 +40,24 @@ describe('PopupComponent', () => {
}); });
}); });
describe('toggleFocusedButton', () => {
it('should change false to true', () => {
component.isButtonFocused = false;
component.toggleFocusedButton();
expect(component.isButtonFocused).toBe(true);
});
it('should change true to false', () => {
component.isButtonFocused = true;
component.toggleFocusedButton();
expect(component.isButtonFocused).toBe(false);
});
});
describe('handleButtonClick', () => { describe('handleButtonClick', () => {
beforeEach(() => { beforeEach(() => {
component.togglePopup = jest.fn(); component.togglePopup = jest.fn();
...@@ -80,12 +98,34 @@ describe('PopupComponent', () => { ...@@ -80,12 +98,34 @@ describe('PopupComponent', () => {
})); }));
}); });
describe('on focus button', () => {
it('should toggle focused button', () => {
component.toggleFocusedButton = jest.fn();
const buttonElement: HTMLElement = getElementFromFixture(fixture, dropdownButton);
buttonElement.focus();
expect(component.toggleFocusedButton).toHaveBeenCalled();
});
});
describe('on blur button', () => {
it('should toggle focused button', () => {
component.toggleFocusedButton = jest.fn();
const buttonElement: HTMLElement = getElementFromFixture(fixture, dropdownButton);
buttonElement.dispatchEvent(new Event('blur'));
expect(component.toggleFocusedButton).toHaveBeenCalled();
});
});
describe('aria-expanded', () => { describe('aria-expanded', () => {
it('should be true if popup is open', () => { it('should be true if popup is open', () => {
component.isPopupOpen = true; component.isPopupOpen = true;
fixture.detectChanges(); fixture.detectChanges();
const buttonElement: HTMLElement = getElementFromFixture(fixture, popupButton); const buttonElement: HTMLElement = getElementFromFixture(fixture, dropdownButton);
expect(buttonElement.getAttribute('aria-expanded')).toBe('true'); expect(buttonElement.getAttribute('aria-expanded')).toBe('true');
}); });
...@@ -94,7 +134,7 @@ describe('PopupComponent', () => { ...@@ -94,7 +134,7 @@ describe('PopupComponent', () => {
component.isPopupOpen = false; component.isPopupOpen = false;
fixture.detectChanges(); fixture.detectChanges();
const buttonElement: HTMLElement = getElementFromFixture(fixture, popupButton); const buttonElement: HTMLElement = getElementFromFixture(fixture, dropdownButton);
expect(buttonElement.getAttribute('aria-expanded')).toBe('false'); expect(buttonElement.getAttribute('aria-expanded')).toBe('false');
}); });
......
...@@ -4,24 +4,28 @@ import { Component, ElementRef, HostListener, Input, ViewChild } from '@angular/ ...@@ -4,24 +4,28 @@ import { Component, ElementRef, HostListener, Input, ViewChild } from '@angular/
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
@Component({ @Component({
selector: 'ods-popup', selector: 'ods-dropdown-menu',
standalone: true, standalone: true,
imports: [CommonModule, CdkTrapFocus], imports: [CommonModule, CdkTrapFocus],
template: `<div class="relative w-fit"> template: `<div class="relative w-fit">
<button <button
[ngClass]="[twMerge('w-fit outline-2 outline-offset-2 outline-focus', buttonClass)]" [ngClass]="[twMerge('block w-fit outline-2 outline-offset-2 outline-focus', buttonClass)]"
(click)="handleButtonClick()" (click)="handleButtonClick()"
[attr.aria-expanded]="isPopupOpen" [attr.aria-expanded]="isPopupOpen"
aria-haspopup="true" aria-haspopup="true"
[attr.aria-label]="label" [attr.aria-label]="label"
data-test-id="popup-button" [attr.data-test-id]="buttonTestId"
[class.ods-focused]="isButtonFocused"
(focus)="toggleFocusedButton()"
(blur)="toggleFocusedButton()"
#button #button
> >
<ng-content select="[button-content]" /> <ng-content select="[button-content]" />
</button> </button>
<ul <div
*ngIf="isPopupOpen" *ngIf="isPopupOpen"
class="absolute max-h-120 min-w-44 max-w-80 animate-fadeIn overflow-y-auto rounded shadow-lg shadow-grayborder focus:outline-none" class="bg-dropdownBg absolute max-h-120 min-w-44 max-w-80
animate-fadeIn overflow-y-auto rounded shadow-md focus:outline-none"
[ngClass]="alignTo === 'left' ? 'right-0' : 'left-0'" [ngClass]="alignTo === 'left' ? 'right-0' : 'left-0'"
role="menu" role="menu"
aria-modal="true" aria-modal="true"
...@@ -30,15 +34,17 @@ import { twMerge } from 'tailwind-merge'; ...@@ -30,15 +34,17 @@ import { twMerge } from 'tailwind-merge';
#popupList #popupList
> >
<ng-content /> <ng-content />
</ul> </div>
</div>`, </div>`,
}) })
export class PopupComponent { export class DropdownMenuComponent {
@Input() alignTo: 'left' | 'right' = 'left'; @Input() alignTo: 'left' | 'right' = 'left';
@Input() label: string = ''; @Input() label: string = '';
@Input() buttonClass: string = ''; @Input() buttonClass: string = '';
@Input() buttonTestId: string = 'dropdown-button';
isPopupOpen: boolean = false; isPopupOpen: boolean = false;
isButtonFocused: boolean = false;
readonly twMerge = twMerge; readonly twMerge = twMerge;
@ViewChild('button') buttonRef: ElementRef<HTMLButtonElement>; @ViewChild('button') buttonRef: ElementRef<HTMLButtonElement>;
...@@ -71,6 +77,10 @@ export class PopupComponent { ...@@ -71,6 +77,10 @@ export class PopupComponent {
this.isPopupOpen = !this.isPopupOpen; this.isPopupOpen = !this.isPopupOpen;
} }
toggleFocusedButton(): void {
this.isButtonFocused = !this.isButtonFocused;
}
closePopupAndFocusButton(): void { closePopupAndFocusButton(): void {
this.isPopupOpen = false; this.isPopupOpen = false;
this.buttonRef.nativeElement.focus(); this.buttonRef.nativeElement.focus();
......
...@@ -8,15 +8,20 @@ import { ...@@ -8,15 +8,20 @@ import {
import { SaveIconComponent } from '../../icons/save-icon/save-icon.component'; import { SaveIconComponent } from '../../icons/save-icon/save-icon.component';
import { UserIconComponent } from '../../icons/user-icon/user-icon.component'; import { UserIconComponent } from '../../icons/user-icon/user-icon.component';
import { PopupListItemComponent } from '../popup-list-item/popup-list-item.component'; import { DropdownMenuButtonComponent } from '../dropdown-menu-button/dropdown-menu-button.component';
import { PopupComponent } from './popup.component'; import { DropdownMenuComponent } from './dropdown-menu.component';
const meta: Meta<PopupComponent> = { const meta: Meta<DropdownMenuComponent> = {
title: 'Popup/Popup', title: 'Dropdown menu/Dropdown menu',
component: PopupComponent, component: DropdownMenuComponent,
decorators: [ decorators: [
moduleMetadata({ moduleMetadata({
imports: [PopupComponent, PopupListItemComponent, SaveIconComponent, UserIconComponent], imports: [
DropdownMenuComponent,
DropdownMenuButtonComponent,
SaveIconComponent,
UserIconComponent,
],
}), }),
componentWrapperDecorator((story) => `<div class="flex justify-center mb-32">${story}</div>`), componentWrapperDecorator((story) => `<div class="flex justify-center mb-32">${story}</div>`),
], ],
...@@ -25,7 +30,7 @@ const meta: Meta<PopupComponent> = { ...@@ -25,7 +30,7 @@ const meta: Meta<PopupComponent> = {
}; };
export default meta; export default meta;
type Story = StoryObj<PopupComponent>; type Story = StoryObj<DropdownMenuComponent>;
export const Default: Story = { export const Default: Story = {
args: { alignTo: 'left', label: '', buttonClass: '' }, args: { alignTo: 'left', label: '', buttonClass: '' },
...@@ -42,37 +47,37 @@ export const Default: Story = { ...@@ -42,37 +47,37 @@ export const Default: Story = {
}, },
render: (args) => ({ render: (args) => ({
props: args, props: args,
template: `<ods-popup ${argsToTemplate(args)}> template: `<ods-dropdown-menu ${argsToTemplate(args)}>
<ods-user-icon button-content /> <ods-user-icon button-content />
<ods-popup-list-item caption="Lorem" /> <ods-dropdown-menu-button caption="Lorem" />
<ods-popup-list-item caption="Ipsum" /> <ods-dropdown-menu-button caption="Ipsum" />
<ods-popup-list-item caption="Dolor" /> <ods-dropdown-menu-button caption="Dolor" />
</ods-popup>`, </ods-dropdown-menu>`,
}), }),
}; };
export const LongText: Story = { export const LongText: Story = {
render: (args) => ({ render: (args) => ({
props: args, props: args,
template: `<ods-popup ${argsToTemplate(args)}> template: `<ods-dropdown-menu ${argsToTemplate(args)}>
<p button-content>Trigger popup</p> <p button-content>Trigger popup</p>
<ods-popup-list-item caption="Lorem" /> <ods-dropdown-menu-button caption="Lorem" />
<ods-popup-list-item caption="Lorem ipsum dolor sit amet" /> <ods-dropdown-menu-button caption="Lorem ipsum dolor sit amet" />
</ods-popup>`, </ods-dropdown-menu>`,
}), }),
}; };
export const ItemsWithIcons: Story = { export const ItemsWithIcons: Story = {
render: (args) => ({ render: (args) => ({
props: args, props: args,
template: `<ods-popup ${argsToTemplate(args)}> template: `<ods-dropdown-menu ${argsToTemplate(args)}>
<p button-content>Trigger popup</p> <p button-content>Trigger popup</p>
<ods-popup-list-item caption="Lorem"> <ods-dropdown-menu-button caption="Lorem">
<ods-save-icon icon size="small" /> <ods-save-icon icon size="small" />
</ods-popup-list-item> </ods-dropdown-menu-button>
<ods-popup-list-item caption="Lorem ipsum dolor sit amet"> <ods-dropdown-menu-button caption="Lorem ipsum dolor sit amet">
<ods-save-icon icon size="small" /> <ods-save-icon icon size="small" />
</ods-popup-list-item> </ods-dropdown-menu-button>
</ods-popup>`, </ods-dropdown-menu>`,
}), }),
}; };
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SettingsIconComponent } from './settings-icon.component';
describe('SettingsIconComponent', () => {
let component: SettingsIconComponent;
let fixture: ComponentFixture<SettingsIconComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SettingsIconComponent],
}).compileComponents();
fixture = TestBed.createComponent(SettingsIconComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { NgClass } from '@angular/common';
import { Component, Input } from '@angular/core';
import { IconVariants, iconVariants } from '@ods/system';
import { twMerge } from 'tailwind-merge';
@Component({
selector: 'ods-settings-icon',
standalone: true,
imports: [NgClass],
template: `<svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
[ngClass]="twMerge(iconVariants({ size }), 'fill-neutral-500', class)"
>
<path
d="M 10.490234 2 C 10.011234 2 9.6017656 2.3385938 9.5097656 2.8085938 L 9.1757812 4.5234375 C 8.3550224 4.8338012 7.5961042 5.2674041 6.9296875 5.8144531 L 5.2851562 5.2480469 C 4.8321563 5.0920469 4.33375 5.2793594 4.09375 5.6933594 L 2.5859375 8.3066406 C 2.3469375 8.7216406 2.4339219 9.2485 2.7949219 9.5625 L 4.1132812 10.708984 C 4.0447181 11.130337 4 11.559284 4 12 C 4 12.440716 4.0447181 12.869663 4.1132812 13.291016 L 2.7949219 14.4375 C 2.4339219 14.7515 2.3469375 15.278359 2.5859375 15.693359 L 4.09375 18.306641 C 4.33275 18.721641 4.8321562 18.908906 5.2851562 18.753906 L 6.9296875 18.1875 C 7.5958842 18.734206 8.3553934 19.166339 9.1757812 19.476562 L 9.5097656 21.191406 C 9.6017656 21.661406 10.011234 22 10.490234 22 L 13.509766 22 C 13.988766 22 14.398234 21.661406 14.490234 21.191406 L 14.824219 19.476562 C 15.644978 19.166199 16.403896 18.732596 17.070312 18.185547 L 18.714844 18.751953 C 19.167844 18.907953 19.66625 18.721641 19.90625 18.306641 L 21.414062 15.691406 C 21.653063 15.276406 21.566078 14.7515 21.205078 14.4375 L 19.886719 13.291016 C 19.955282 12.869663 20 12.440716 20 12 C 20 11.559284 19.955282 11.130337 19.886719 10.708984 L 21.205078 9.5625 C 21.566078 9.2485 21.653063 8.7216406 21.414062 8.3066406 L 19.90625 5.6933594 C 19.66725 5.2783594 19.167844 5.0910937 18.714844 5.2460938 L 17.070312 5.8125 C 16.404116 5.2657937 15.644607 4.8336609 14.824219 4.5234375 L 14.490234 2.8085938 C 14.398234 2.3385937 13.988766 2 13.509766 2 L 10.490234 2 z M 12 8 C 14.209 8 16 9.791 16 12 C 16 14.209 14.209 16 12 16 C 9.791 16 8 14.209 8 12 C 8 9.791 9.791 8 12 8 z"
/>
</svg>`,
})
export class SettingsIconComponent {
@Input() size: IconVariants['size'] = 'medium';
@Input() class: string = undefined;
protected readonly iconVariants = iconVariants;
protected readonly twMerge = twMerge;
}
import { moduleMetadata, type Meta, type StoryObj } from '@storybook/angular';
import { PopupListItemComponent } from './popup-list-item.component';
const meta: Meta<PopupListItemComponent> = {
title: 'Popup/Popup list item',
component: PopupListItemComponent,
decorators: [
moduleMetadata({
imports: [PopupListItemComponent],
}),
],
excludeStories: /.*Data$/,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<PopupListItemComponent>;
export const Default: Story = {
args: {
caption: 'List item',
},
};
...@@ -36,6 +36,7 @@ ...@@ -36,6 +36,7 @@
--color-doc: 219 63% 54%; --color-doc: 219 63% 54%;
--color-modal-bg: 0 0% 100%; --color-modal-bg: 0 0% 100%;
--color-dropdown-bg: 0 0% 98%;
} }
.dark { .dark {
...@@ -69,6 +70,7 @@ ...@@ -69,6 +70,7 @@
--color-doc: 219 63% 54%; --color-doc: 219 63% 54%;
--color-modal-bg: 0 0% 26%; --color-modal-bg: 0 0% 26%;
--color-dropdown-bg: 0 0% 26%;
} }
.bescheid-dialog-backdrop { .bescheid-dialog-backdrop {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment