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

Merge pull request 'OZG-6101-logout-admin' (#729) from OZG-6101-logout-admin into master

parents 0f243ae9 fdd0220c
No related branches found
No related tags found
No related merge requests found
Showing
with 546 additions and 8 deletions
...@@ -26,4 +26,6 @@ export * from './lib/icons/spinner-icon/spinner-icon.component'; ...@@ -26,4 +26,6 @@ export * from './lib/icons/spinner-icon/spinner-icon.component';
export * from './lib/icons/stamp-icon/stamp-icon.component'; export * from './lib/icons/stamp-icon/stamp-icon.component';
export * from './lib/instant-search/instant-search/instant-search.component'; 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/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';
...@@ -8,6 +8,7 @@ export const iconVariants = cva('', { ...@@ -8,6 +8,7 @@ export const iconVariants = cva('', {
medium: 'size-6', medium: 'size-6',
large: 'size-8', large: 'size-8',
'extra-large': 'size-10', 'extra-large': 'size-10',
xxl: 'size-12',
}, },
}, },
}); });
......
...@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/angular'; ...@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/angular';
import { OfficeIconComponent } from './office-icon.component'; import { OfficeIconComponent } from './office-icon.component';
const meta: Meta<OfficeIconComponent> = { const meta: Meta<OfficeIconComponent> = {
title: 'Icons/Save icon', title: 'Icons/Office icon',
component: OfficeIconComponent, component: OfficeIconComponent,
excludeStories: /.*Data$/, excludeStories: /.*Data$/,
tags: ['autodocs'], tags: ['autodocs'],
......
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserIconComponent } from './user-icon.component';
describe('UserIconComponent', () => {
let component: UserIconComponent;
let fixture: ComponentFixture<UserIconComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserIconComponent],
}).compileComponents();
fixture = TestBed.createComponent(UserIconComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { twMerge } from 'tailwind-merge';
import { ExclamationIconComponent } from '../exclamation-icon/exclamation-icon.component';
import { IconVariants, iconVariants } from '../iconVariants';
@Component({
selector: 'ods-user-icon',
standalone: true,
imports: [CommonModule, ExclamationIconComponent],
template: `
<svg
viewBox="0 0 47 47"
fill="none"
xmlns="http://www.w3.org/2000/svg"
[ngClass]="[twMerge(iconVariants({ size }), 'fill-ozggray-300', class)]"
>
<path
d="M23.5 3.91663C12.69 3.91663 3.91669 12.69 3.91669 23.5C3.91669 34.31 12.69 43.0833 23.5 43.0833C34.31 43.0833 43.0834 34.31 43.0834 23.5C43.0834 12.69 34.31 3.91663 23.5 3.91663ZM23.5 9.79163C26.7509 9.79163 29.375 12.4158 29.375 15.6666C29.375 18.9175 26.7509 21.5416 23.5 21.5416C20.2492 21.5416 17.625 18.9175 17.625 15.6666C17.625 12.4158 20.2492 9.79163 23.5 9.79163ZM23.5 37.6C18.6042 37.6 14.2763 35.0933 11.75 31.2941C11.8088 27.397 19.5834 25.2625 23.5 25.2625C27.3971 25.2625 35.1913 27.397 35.25 31.2941C32.7238 35.0933 28.3959 37.6 23.5 37.6Z"
/>
</svg>
`,
})
export class UserIconComponent {
@Input() variant: 'user' | 'initials' = 'user';
@Input() size: IconVariants['size'] = 'xxl';
@Input() class: string = undefined;
iconVariants = iconVariants;
twMerge = twMerge;
}
import type { Meta, StoryObj } from '@storybook/angular';
import { UserIconComponent } from './user-icon.component';
const meta: Meta<UserIconComponent> = {
title: 'Icons/User icon',
component: UserIconComponent,
excludeStories: /.*Data$/,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<UserIconComponent>;
export const Default: Story = {
args: { size: 'xxl' },
argTypes: {
size: {
control: 'select',
options: ['small', 'medium', 'large', 'extra-large', 'xxl', 'full'],
description: 'Size of icon. Property "full" means 100%',
table: {
defaultValue: { summary: 'xxl' },
},
},
},
};
...@@ -26,14 +26,12 @@ export const SearchResults: Story = { ...@@ -26,14 +26,12 @@ export const SearchResults: Story = {
headerText: 'In der OZG-Cloud', headerText: 'In der OZG-Cloud',
searchResults: [ searchResults: [
{ {
text: 'Landeshauptstadt Kiel - Ordnungsamt, Gewerbe- und Schornsteinfegeraufsicht', title: 'Landeshauptstadt Kiel - Ordnungsamt, Gewerbe- und Schornsteinfegeraufsicht',
subText: 'Fabrikstraße 8-10, 24103 Kiel', description: 'Fabrikstraße 8-10, 24103 Kiel',
onClick: () => undefined,
}, },
{ {
text: 'Amt für Digitalisierung, Breitband und Vermessung Nürnberg Außenstelle Hersbruck', title: 'Amt für Digitalisierung, Breitband und Vermessung Nürnberg Außenstelle Hersbruck',
subText: 'Rathausmarkt 7, Hersbruck', description: 'Rathausmarkt 7, Hersbruck',
onClick: () => undefined,
}, },
], ],
}, },
......
import { dispatchEventFromFixture } from '@alfa-client/test-utils';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PopupListItemComponent } from './popup-list-item.component';
describe('PopupListItemComponent', () => {
let component: PopupListItemComponent;
let fixture: ComponentFixture<PopupListItemComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PopupListItemComponent],
}).compileComponents();
fixture = TestBed.createComponent(PopupListItemComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('itemClicked emitter', () => {
it('should emit itemClicked', () => {
component.itemClicked.emit = jest.fn();
dispatchEventFromFixture(fixture, 'button', 'click');
expect(component.itemClicked.emit).toHaveBeenCalled();
});
});
});
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'ods-popup-list-item',
standalone: true,
imports: [CommonModule],
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"
role="listitem"
(click)="itemClicked.emit()"
>
<ng-content select="[icon]" />
<p class="text-text">{{ caption }}</p>
</button>`,
})
export class PopupListItemComponent {
@Input({ required: true }) caption!: string;
@Output() itemClicked: EventEmitter<MouseEvent> = new EventEmitter();
}
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',
},
};
import { getElementFromFixture } from '@alfa-client/test-utils';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { getDataTestIdOf } from 'libs/tech-shared/test/data-test';
import { PopupComponent } from './popup.component';
describe('PopupComponent', () => {
let component: PopupComponent;
let fixture: ComponentFixture<PopupComponent>;
const popupButton: string = getDataTestIdOf('popup-button');
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PopupComponent],
}).compileComponents();
fixture = TestBed.createComponent(PopupComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('togglePopup', () => {
it('should change false to true', () => {
component.isPopupOpen = false;
component.togglePopup();
expect(component.isPopupOpen).toBe(true);
});
it('should change true to false', () => {
component.isPopupOpen = true;
component.togglePopup();
expect(component.isPopupOpen).toBe(false);
});
});
describe('aria-expanded', () => {
it('should be true if popup is open', () => {
component.isPopupOpen = true;
fixture.detectChanges();
const buttonElement: HTMLElement = getElementFromFixture(fixture, popupButton);
expect(buttonElement.getAttribute('aria-expanded')).toBe('true');
});
it('should be false if popup is closed', () => {
component.isPopupOpen = false;
fixture.detectChanges();
const buttonElement: HTMLElement = getElementFromFixture(fixture, popupButton);
expect(buttonElement.getAttribute('aria-expanded')).toBe('false');
});
});
describe('isEscapeKey', () => {
it('should return true', () => {
const escapeKeyEvent: KeyboardEvent = {
...new KeyboardEvent('esc'),
key: 'Escape',
};
const result: boolean = component.isEscapeKey(escapeKeyEvent);
expect(result).toBe(true);
});
it('should return false', () => {
const keyEvent: KeyboardEvent = new KeyboardEvent('whatever');
const result: boolean = component.isEscapeKey(keyEvent);
expect(result).toBe(false);
});
});
describe('closePopupAndFocusButton', () => {
beforeEach(() => {
component.isPopupOpen = true;
jest.spyOn(component.buttonRef.nativeElement, 'focus');
});
it('should close popup', () => {
component.closePopupAndFocusButton();
expect(component.isPopupOpen).toBe(false);
});
it('should focus button', () => {
component.closePopupAndFocusButton();
expect(component.buttonRef.nativeElement.focus).toHaveBeenCalled();
});
});
describe('isPopupClosed', () => {
it('should return true', () => {
component.isPopupOpen = false;
const result: boolean = component.isPopupClosed();
expect(result).toBe(true);
});
it('should return false', () => {
component.isPopupOpen = true;
const result: boolean = component.isPopupClosed();
expect(result).toBe(false);
});
});
describe('onKeydownHandler', () => {
const e: KeyboardEvent = new KeyboardEvent('test');
beforeEach(() => {
component.closePopupAndFocusButton = jest.fn();
component.isEscapeKey = jest.fn();
});
describe('popup is closed', () => {
beforeEach(() => {
component.isPopupClosed = jest.fn().mockReturnValue(true);
});
it('should not check for escape key', () => {
component.onKeydownHandler(e);
expect(component.isEscapeKey).not.toHaveBeenCalled();
});
});
describe('popup is open', () => {
beforeEach(() => {
component.isPopupClosed = jest.fn().mockReturnValue(false);
});
it('should check for escape key', () => {
component.onKeydownHandler(e);
expect(component.isEscapeKey).toHaveBeenCalled();
});
it('should handle escape key', () => {
component.isEscapeKey = jest.fn().mockReturnValue(true);
component.onKeydownHandler(e);
expect(component.closePopupAndFocusButton).toHaveBeenCalled();
});
it('should not handle escape key', () => {
component.onKeydownHandler(e);
expect(component.closePopupAndFocusButton).not.toHaveBeenCalled();
});
});
});
describe('onClickHandler', () => {
const e: MouseEvent = new MouseEvent('test');
beforeEach(() => {
component.closePopupAndFocusButton = jest.fn();
component.buttonRef.nativeElement.contains = jest.fn();
});
describe('popup is closed', () => {
beforeEach(() => {
component.isPopupClosed = jest.fn().mockReturnValue(true);
});
it('should not check for button containing event target', () => {
component.onClickHandler(e);
expect(component.buttonRef.nativeElement.contains).not.toHaveBeenCalled();
});
});
describe('popup is open', () => {
beforeEach(() => {
component.isPopupClosed = jest.fn().mockReturnValue(false);
});
it('should check for button containing event target', () => {
component.onClickHandler(e);
expect(component.buttonRef.nativeElement.contains).toHaveBeenCalled();
});
it('should handle click', () => {
component.onClickHandler(e);
expect(component.closePopupAndFocusButton).toHaveBeenCalled();
});
it('should not handle click', () => {
component.buttonRef.nativeElement.contains = jest.fn().mockReturnValue(true);
component.onClickHandler(e);
expect(component.closePopupAndFocusButton).not.toHaveBeenCalled();
});
});
});
});
import { CdkTrapFocus } from '@angular/cdk/a11y';
import { CommonModule } from '@angular/common';
import { Component, ElementRef, HostListener, Input, ViewChild } from '@angular/core';
import { twMerge } from 'tailwind-merge';
@Component({
selector: 'ods-popup',
standalone: true,
imports: [CommonModule, CdkTrapFocus],
template: `<div class="relative w-fit">
<button
class="w-fit outline-2 outline-offset-2 outline-focus"
[ngClass]="[twMerge('w-fit outline-2 outline-offset-2 outline-focus', buttonClass)]"
(click)="togglePopup()"
[attr.aria-expanded]="isPopupOpen"
aria-haspopup="true"
[attr.aria-label]="label"
data-test-id="popup-button"
#button
>
<ng-content select="[button]" />
</button>
<ul
*ngIf="isPopupOpen"
class="max-h-120 animate-fadeIn absolute min-w-44 max-w-80 overflow-y-auto rounded shadow-lg shadow-grayborder focus:outline-none"
[ngClass]="alignTo === 'left' ? 'right-0' : 'left-0'"
role="dialog"
aria-modal="true"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<ng-content />
</ul>
</div>`,
})
export class PopupComponent {
@Input() alignTo: 'left' | 'right' = 'left';
@Input() label: string = '';
@Input() buttonClass: string = '';
isPopupOpen: boolean = false;
twMerge = twMerge;
@ViewChild('button') buttonRef: ElementRef<HTMLButtonElement>;
@HostListener('document:keydown', ['$event'])
onKeydownHandler(e: KeyboardEvent): void {
if (this.isPopupClosed()) return;
if (this.isEscapeKey(e)) this.closePopupAndFocusButton();
}
@HostListener('document:click', ['$event'])
onClickHandler(e: MouseEvent): void {
if (this.isPopupClosed()) return;
if (!this.buttonRef.nativeElement.contains(e.target as HTMLElement)) {
this.closePopupAndFocusButton();
}
}
togglePopup(): void {
this.isPopupOpen = !this.isPopupOpen;
}
closePopupAndFocusButton(): void {
this.isPopupOpen = false;
this.buttonRef.nativeElement.focus();
}
isEscapeKey(e: KeyboardEvent): boolean {
return e.key === 'Escape';
}
isPopupClosed(): boolean {
return !this.isPopupOpen;
}
}
import {
argsToTemplate,
componentWrapperDecorator,
moduleMetadata,
type Meta,
type StoryObj,
} from '@storybook/angular';
import { SaveIconComponent } from '../../icons/save-icon/save-icon.component';
import { UserIconComponent } from '../../icons/user-icon/user-icon.component';
import { PopupListItemComponent } from '../popup-list-item/popup-list-item.component';
import { PopupComponent } from './popup.component';
const meta: Meta<PopupComponent> = {
title: 'Popup/Popup',
component: PopupComponent,
decorators: [
moduleMetadata({
imports: [PopupComponent, PopupListItemComponent, SaveIconComponent, UserIconComponent],
}),
componentWrapperDecorator((story) => `<div class="flex justify-center mb-32">${story}</div>`),
],
excludeStories: /.*Data$/,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<PopupComponent>;
export const Default: Story = {
args: { alignTo: 'left', label: '', buttonClass: '' },
argTypes: {
alignTo: {
control: 'select',
options: ['left', 'right'],
table: {
defaultValue: { summary: 'left' },
},
},
buttonClass: { description: 'Tailwind class for button' },
label: { description: 'Aria-label for button' },
},
render: (args) => ({
props: args,
template: `<ods-popup ${argsToTemplate(args)}>
<ods-user-icon button />
<ods-popup-list-item caption="Lorem" />
<ods-popup-list-item caption="Ipsum" />
<ods-popup-list-item caption="Dolor" />
</ods-popup>`,
}),
};
export const LongText: Story = {
render: (args) => ({
props: args,
template: `<ods-popup ${argsToTemplate(args)}>
<p button>Trigger popup</p>
<ods-popup-list-item caption="Lorem" />
<ods-popup-list-item caption="Lorem ipsum dolor sit amet" />
</ods-popup>`,
}),
};
export const ItemsWithIcons: Story = {
render: (args) => ({
props: args,
template: `<ods-popup ${argsToTemplate(args)}>
<p button>Trigger popup</p>
<ods-popup-list-item caption="Lorem">
<ods-save-icon icon size="small" />
</ods-popup-list-item>
<ods-popup-list-item caption="Lorem ipsum dolor sit amet">
<ods-save-icon icon size="small" />
</ods-popup-list-item>
</ods-popup>`,
}),
};
...@@ -13,7 +13,11 @@ module.exports = { ...@@ -13,7 +13,11 @@ module.exports = {
darkMode: 'class', darkMode: 'class',
theme: { theme: {
extend: { extend: {
animation: { dash: 'dash 1.5s ease-in-out infinite', 'spin-slow': 'spin 2s linear infinite' }, animation: {
dash: 'dash 1.5s ease-in-out infinite',
'spin-slow': 'spin 2s linear infinite',
fadeIn: 'fade-in 0.2s ease-in-out 1',
},
keyframes: { keyframes: {
dash: { dash: {
from: { from: {
...@@ -29,10 +33,21 @@ module.exports = { ...@@ -29,10 +33,21 @@ module.exports = {
'stroke-dashoffset': '-49', 'stroke-dashoffset': '-49',
}, },
}, },
'fade-in': {
'0%': {
opacity: 0,
},
'100%': {
opacity: 1,
},
},
}, },
borderWidth: { borderWidth: {
3: '3px', 3: '3px',
}, },
maxHeight: {
120: '480px',
},
colors: { colors: {
ozgblue: { ozgblue: {
50: 'hsl(200, 100%, 96%)', 50: 'hsl(200, 100%, 96%)',
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment