Skip to content
Snippets Groups Projects
Commit 54e42ad4 authored by Martin's avatar Martin
Browse files

Merge remote-tracking branch 'origin/main' into OZG-6810-FMDateiinhaltErsetzen

parents 26adf588 91d3a77f
No related branches found
No related tags found
1 merge request!14Ozg 6810 fm dateiinhalt ersetzen
......@@ -156,9 +156,9 @@ services:
environment:
- KEYCLOAK_URL=https://sso.dev.by.ozg-cloud.de
- OZGCLOUD_KEYCLOAK_API_CLIENT=alfa
- OZGCLOUD_KEYCLOAK_API_PASSWORD=
- OZGCLOUD_KEYCLOAK_API_REALM=${KEYCLOAK_REALM:-by-e2e-tests-local-dev}
- OZGCLOUD_KEYCLOAK_API_USER=usermanagerapiuser
- OZGCLOUD_KEYCLOAK_API_PASSWORD=${OZGCLOUD_KEYCLOAK_API_PASSWORD:-}
- OZGCLOUD_USER_MANAGER_URL=http://localhost:9092
- OZGCLOUD_USERSYNC_PERIOD=disabled
- OZGCLOUD_USERSYNC_ONSTART=false
......
......@@ -45,6 +45,7 @@
href="#"
class="flex flex-col items-start justify-between gap-2 rounded-t-md border-primary-600/50 px-6 py-4 hover:bg-background-150 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus lg:flex-row lg:gap-6"
tooltip="This is tooltip attached to link element"
tooltipPosition="above"
>
<div class="flex-1 basis-5/6">
<div class="flex flex-wrap items-center gap-x-3">
......@@ -98,6 +99,7 @@
</li>
</ul>
</div>
<div class="my-4">
<h1 class="mb-6 text-2xl font-semibold text-text">Benutzer & Rollen</h1>
<ods-button text="Benutzer hinzufügen" />
......@@ -319,7 +321,22 @@
</li>
</ul>
</div>
<div class="my-12">
<h1 class="mb-6 text-2xl font-semibold text-text">Tooltips</h1>
<div class="my-4 flex flex-wrap gap-4">
<ods-button
text="Button with tooltip"
tooltip="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut laboreetdolorelaboreetdolorelaboreetdolorelaboreetdolorelaboreetdolorelaboreetdolorelaboreetdolorelaboreetdolorelaboreetdolorelaboreetdolorelaboreetdolorelaboreetdolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy "
/>
<ods-button
text="Button 2"
tooltip="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut"
/><ods-button
text="Button 2"
tooltip="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut"
/>
</div>
</div>
<div class="my-5">
<ods-instant-search
headerText="In der OZG-Cloud"
......
.tooltip::before {
left: calc(50% - 0.5rem - var(--before-left));
}
......@@ -23,6 +23,7 @@
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TooltipComponent } from './tooltip.component';
import { TooltipPosition } from './tooltip.directive';
describe('TooltipComponent', () => {
let component: TooltipComponent;
......@@ -41,4 +42,20 @@ describe('TooltipComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
describe('component', () => {
describe('set position', () => {
it('should set class for position above', () => {
component.position = TooltipPosition.ABOVE;
expect(component.class).toContain('border-t');
});
it('should set class for position below', () => {
component.position = TooltipPosition.BELOW;
expect(component.class).toContain('border-b');
});
});
});
});
......@@ -21,22 +21,28 @@
* Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen.
*/
import { NgClass } from '@angular/common';
import { Component } from '@angular/core';
import { TooltipPosition } from './tooltip.directive';
@Component({
selector: 'ods-tooltip',
template: `<p
class="fixed z-[100] mt-2 hidden -translate-x-1/2 animate-fadeIn cursor-default rounded bg-ozggray-900 px-3 py-2 text-sm text-whitetext before:absolute before:-top-2 before:left-[calc(50%-0.5rem)] before:size-0 before:border-b-[0.5rem] before:border-l-[0.5rem] before:border-r-[0.5rem] before:border-b-ozggray-900 before:border-l-transparent before:border-r-transparent before:content-[''] dark:bg-white dark:before:border-b-white"
[class.block]="show"
[class.hidden]="!show"
[style.left]="left + 'px'"
[style.top]="top + 'px'"
imports: [NgClass],
template: `<span
class="tooltip fixed z-[100] max-w-xs animate-fadeIn cursor-default break-words rounded bg-ozggray-900 px-3 py-2 text-sm text-whitetext before:absolute before:border-l-[0.5rem] before:border-r-[0.5rem] before:border-l-transparent before:border-r-transparent dark:bg-white md:max-w-[calc(90vw)]"
[ngClass]="class"
[class.visible]="show"
[class.invisible]="!show"
[style.left.px]="left"
[style.top.px]="top"
[style.--before-left.px]="leftOffset"
[attr.id]="id"
role="tooltip"
>
{{ text }}
</p>`,
</span>`,
styles: [':host {@apply contents}'],
styleUrl: './tooltip.component.scss',
standalone: true,
})
export class TooltipComponent {
......@@ -45,4 +51,16 @@ export class TooltipComponent {
top: number = 0;
show: boolean = false;
id: string;
class: string;
leftOffset: number;
set position(value: TooltipPosition) {
if (value === TooltipPosition.ABOVE) {
this.class =
'mb-2 -translate-x-1/2 -translate-y-[calc(100%+0.5rem)] before:-bottom-2 before:border-t-[0.5rem] before:border-t-ozggray-900 dark:before:border-t-white';
} else {
this.class =
'mt-2 -translate-x-1/2 before:-top-2 before:border-b-[0.5rem] before:border-b-ozggray-900 dark:before:border-b-white';
}
}
}
......@@ -21,11 +21,13 @@
* Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen.
*/
import { EMPTY_STRING } from '@alfa-client/tech-shared';
import { InteractivityChecker } from '@angular/cdk/a11y';
import { ComponentRef, ElementRef, Renderer2, ViewContainerRef } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { faker } from '@faker-js/faker/.';
import { TooltipComponent } from './tooltip.component';
import { TooltipDirective } from './tooltip.directive';
import { TooltipDirective, TooltipPosition } from './tooltip.directive';
class MockElementRef extends ElementRef {
nativeElement = {
......@@ -45,8 +47,9 @@ describe('TooltipDirective', () => {
location: null,
hostView: null,
injector: null,
instance: { id: '', left: 0, top: 0, text: '', show: false },
instance: { id: '', left: 0, top: 0, text: '', show: false, position: TooltipPosition.BELOW, class: '', leftOffset: 0 },
};
const parentRect: DOMRect = { bottom: 0, top: 0, height: 0, left: 0, right: 0, toJSON: jest.fn(), width: 0, x: 0, y: 0 };
beforeEach((): void => {
TestBed.configureTestingModule({
......@@ -84,9 +87,11 @@ describe('TooltipDirective', () => {
describe('createTooltip', () => {
beforeEach(() => {
directive.viewContainerRef.createComponent = jest.fn().mockReturnValue({ location: { nativeElement: {} } });
directive.setAriaLabeledBy = jest.fn();
directive.getFocusableElement = jest.fn().mockReturnValue({ appendChild: jest.fn() });
directive.setAriaAttribute = jest.fn();
directive.setInitialTooltipProperties = jest.fn();
directive.getParentElement = jest.fn().mockReturnValue({ appendChild: jest.fn() });
directive.interactivityChecker.isFocusable = jest.fn();
directive.tooltip = faker.lorem.sentence();
});
it('should create tooltip component', () => {
......@@ -95,28 +100,36 @@ describe('TooltipDirective', () => {
expect(directive.viewContainerRef.createComponent).toHaveBeenCalled();
});
it('should check if parent element focusable', () => {
it('should get parent element', () => {
directive.createTooltip();
expect(directive.interactivityChecker.isFocusable).toHaveBeenCalled();
expect(directive.getParentElement).toHaveBeenCalled();
});
it('should get focusable element if parent not focusable', () => {
it('should insert tooltip component to parent element', () => {
directive.createTooltip();
expect(directive.getFocusableElement).toHaveBeenCalled();
expect(directive.parentElement.appendChild).toHaveBeenCalled();
});
it('should set initial tooltip properties', () => {
directive.createTooltip();
expect(directive.setInitialTooltipProperties).toHaveBeenCalled();
});
it('should insert tooltip component to focusable', () => {
it('should set aria attribute to parent', () => {
directive.createTooltip();
expect(directive.focusableElement.appendChild).toHaveBeenCalled();
expect(directive.setAriaAttribute).toHaveBeenCalled();
});
it('should set aria-labeledby attribute to parent', () => {
it('should not create tooltip', () => {
directive.tooltip = EMPTY_STRING;
directive.createTooltip();
expect(directive.setAriaLabeledBy).toHaveBeenCalled();
expect(directive.getParentElement).not.toHaveBeenCalled();
});
});
......@@ -132,10 +145,16 @@ describe('TooltipDirective', () => {
expect(directive.elementRef.nativeElement.contains).toHaveBeenCalled();
});
it('should set attachedToFocused field', () => {
directive.showTooltip();
expect(directive.attachedToFocused).toBeTruthy();
});
it('should set tooltip properties', () => {
directive.showTooltip();
expect(directive.setTooltipProperties).toHaveBeenCalledWith(true);
expect(directive.setTooltipProperties).toHaveBeenCalled();
});
});
......@@ -160,12 +179,30 @@ describe('TooltipDirective', () => {
});
});
describe('setInitialTooltipProperties', () => {
beforeEach(() => {
directive.componentRef = mockComponentRef;
});
it('should set tooltip text', () => {
directive.setInitialTooltipProperties('Test', 'tooltip-1');
expect(directive.componentRef.instance.text).toBe('Test');
});
it('should set tooltip id', () => {
directive.setInitialTooltipProperties('Test', 'tooltip-1');
expect(directive.componentRef.instance.id).toBe('tooltip-1');
});
});
describe('setTooltipProperties', () => {
beforeEach(() => {
directive.componentRef = mockComponentRef;
directive.elementRef.nativeElement.getBoundingClientRect = jest
.fn()
.mockReturnValue({ left: 0, right: 1000, bottom: 1000 });
directive.setTooltipOffsetAndPosition = jest.fn();
directive.elementRef.nativeElement.getBoundingClientRect = jest.fn().mockReturnValue({ left: 0, right: 1000 });
directive.getTopPosition = jest.fn().mockReturnValue(1000);
});
it('should get bounding client rect', () => {
......@@ -174,49 +211,208 @@ describe('TooltipDirective', () => {
expect(directive.elementRef.nativeElement.getBoundingClientRect).toHaveBeenCalled();
});
it('should get top position for tooltip', () => {
directive.setTooltipProperties();
expect(directive.getTopPosition).toHaveBeenCalled();
});
it('should set tooltip instance properties', () => {
directive.tooltip = 'I am tooltip';
directive.tooltipId = 'tooltip-1';
directive.setTooltipProperties();
expect(directive.componentRef.instance).toStrictEqual({
id: 'tooltip-1',
expect(directive.componentRef.instance).toMatchObject({
class: '',
left: 500,
text: 'I am tooltip',
top: 1000,
show: true,
});
});
});
describe('setAutoPosition', () => {
it('should set tooltip position above if it cannot be displayed below', () => {
directive.tooltipPosition = TooltipPosition.BELOW;
const rectForAbove: DOMRect = { ...parentRect, bottom: 980 };
it('should add margin if parent element focused', () => {
directive.setTooltipProperties(true);
directive.setAutoPosition(36, rectForAbove, 1000);
expect(directive.componentRef.instance.top).toBe(1004);
expect(directive.position).toBe(TooltipPosition.ABOVE);
});
it('should set tooltip position below if it cannot be displayed above', () => {
directive.tooltipPosition = TooltipPosition.ABOVE;
const rectForBelow: DOMRect = { ...parentRect, top: 15 };
directive.setAutoPosition(36, rectForBelow, 1000);
expect(directive.position).toBe(TooltipPosition.BELOW);
});
it('should set tooltip on selected position otherwise', () => {
directive.tooltipPosition = TooltipPosition.ABOVE;
const rectForStandard: DOMRect = { ...parentRect, top: 500 };
directive.setAutoPosition(36, rectForStandard, 1000);
expect(directive.position).toBe(TooltipPosition.ABOVE);
});
});
describe('setLeftOffset', () => {
beforeEach(() => {
directive.leftOffset = 0;
});
it('should set 0 offset, if parent wider than tooltip', () => {
const rectForStandard: DOMRect = { ...parentRect, left: 50, right: 500, width: 100 };
directive.setLeftOffset(36, rectForStandard, 1000);
expect(directive.leftOffset).toBe(0);
});
describe('setAriaLabeledBy', () => {
it('should set positive left offset', () => {
const rectForLeft: DOMRect = { ...parentRect, left: 10 };
directive.setLeftOffset(50, rectForLeft, 1000);
expect(directive.leftOffset).toBe(15);
});
it('should set negative left offset', () => {
const rectForRight: DOMRect = { ...parentRect, left: 50, right: 990 };
directive.setLeftOffset(50, rectForRight, 1000);
expect(directive.leftOffset).toBe(-15);
});
it('should set 0 offset', () => {
const rectForStandard: DOMRect = { ...parentRect, left: 50, right: 500 };
directive.setLeftOffset(36, rectForStandard, 1000);
expect(directive.leftOffset).toBe(0);
});
});
describe('setTooltipOffsetAndPosition', () => {
beforeEach(() => {
directive.setAutoPosition = jest.fn();
directive.setLeftOffset = jest.fn();
directive.componentRef = Object.assign(mockComponentRef, {
location: {
nativeElement: { children: [{ getBoundingClientRect: jest.fn().mockReturnValue({ height: 100, width: 200 }) }] },
},
});
directive.elementRef.nativeElement.getBoundingClientRect = jest.fn().mockReturnValue({});
window.innerHeight = 404;
window.innerWidth = 503;
});
it('should set tooltip auto position', () => {
directive.setTooltipOffsetAndPosition();
expect(directive.setAutoPosition).toHaveBeenCalledWith(100, {}, 404);
});
it('should set tooltip left offset', () => {
directive.setTooltipOffsetAndPosition();
expect(directive.setLeftOffset).toHaveBeenCalledWith(200, {}, 503);
});
});
describe('setAriaAttribute', () => {
beforeEach(() => {
directive.renderer.setAttribute = jest.fn();
directive.parentElement = null;
directive.tooltipId = 'test';
});
it('should set aria-labeledby attribute', () => {
directive.setAriaLabeledBy();
it('should set aria-describedby attribute', () => {
directive.setAriaAttribute('aria-describedby');
expect(directive.renderer.setAttribute).toHaveBeenCalled();
expect(directive.renderer.setAttribute).toHaveBeenCalledWith(null, 'aria-describedby', 'test');
});
it('should set aria-labelledby attribute', () => {
directive.setAriaAttribute('aria-labelledby');
expect(directive.renderer.setAttribute).toHaveBeenCalledWith(null, 'aria-labelledby', 'test');
});
});
describe('removeAriaLabeledBy', () => {
describe('removeAriaLabelledBy', () => {
beforeEach(() => {
directive.renderer.removeAttribute = jest.fn();
directive.parentElement = null;
});
it('should remove aria-describedby attribute', () => {
directive.removeAriaAttribute('aria-describedby');
expect(directive.renderer.removeAttribute).toHaveBeenCalledWith(null, 'aria-describedby');
});
it('should remove aria-labelledby attribute', () => {
directive.removeAriaAttribute('aria-labelledby');
expect(directive.renderer.removeAttribute).toHaveBeenCalledWith(null, 'aria-labelledby');
});
});
describe('getParentElement', () => {
beforeEach(() => {
directive.getFocusableElement = jest.fn();
directive.interactivityChecker.isFocusable = jest.fn();
});
it('should check if element focusable', () => {
const element = document.createElement('button');
directive.getParentElement(element);
expect(directive.interactivityChecker.isFocusable).toHaveBeenCalled();
});
it('should return element itself if it is focusable', () => {
const element = document.createElement('button');
directive.interactivityChecker.isFocusable = jest.fn().mockReturnValue(true);
const result: HTMLElement = directive.getParentElement(element);
expect(result).toBe(element);
});
it('should get focusable element from the given element', () => {
const element = document.createElement('button');
directive.getParentElement(element);
expect(directive.getFocusableElement).toHaveBeenCalled();
});
it('should return focusable element if it is not null', () => {
const element = document.createElement('div');
const subElement = document.createElement('button');
directive.getFocusableElement = jest.fn().mockReturnValue(subElement);
const result: HTMLElement = directive.getParentElement(element);
expect(result).toBe(subElement);
});
it('should remove aria-labeledby attribute', () => {
directive.removeAriaLabeledBy();
it('should return element itself if focusable element is null', () => {
const element = document.createElement('div');
directive.getFocusableElement = jest.fn().mockReturnValue(null);
expect(directive.renderer.removeAttribute).toHaveBeenCalled();
const result: HTMLElement = directive.getParentElement(element);
expect(result).toBe(element);
});
});
......@@ -240,6 +436,32 @@ describe('TooltipDirective', () => {
});
});
describe('getTopPosition', () => {
it('should return top for position above', () => {
const result: number = directive.getTopPosition(123, 456, TooltipPosition.ABOVE, false);
expect(result).toBe(123);
});
it('should return top for position above with offset', () => {
const result: number = directive.getTopPosition(123, 456, TooltipPosition.ABOVE, true);
expect(result).toBe(119);
});
it('should return top for position below', () => {
const result: number = directive.getTopPosition(123, 456, TooltipPosition.BELOW, false);
expect(result).toBe(456);
});
it('should return top for position below with offset', () => {
const result: number = directive.getTopPosition(123, 456, TooltipPosition.BELOW, true);
expect(result).toBe(460);
});
});
describe('hide', () => {
beforeEach(() => {
directive.componentRef = Object.assign(mockComponentRef, { instance: { ...mockComponentRef.instance, show: true } });
......@@ -248,14 +470,14 @@ describe('TooltipDirective', () => {
it('should hide component', () => {
directive.hide();
expect(directive.componentRef.instance.show).toBeFalsy;
expect(directive.componentRef.instance.show).toBeFalsy();
});
});
describe('destroy', () => {
beforeEach(() => {
directive.componentRef = mockComponentRef;
directive.removeAriaLabeledBy = jest.fn();
directive.removeAriaAttribute = jest.fn();
});
it('should set component ref to null', () => {
......@@ -264,16 +486,16 @@ describe('TooltipDirective', () => {
expect(directive.componentRef).toBeNull();
});
it('should remove aria-labeledby attribute', () => {
it('should remove aria attribute', () => {
directive.destroy();
expect(directive.removeAriaLabeledBy).toHaveBeenCalled();
expect(directive.removeAriaAttribute).toHaveBeenCalled();
});
it('should set focusable element to null', () => {
directive.destroy();
expect(directive.focusableElement).toBeNull();
expect(directive.parentElement).toBeNull();
});
});
});
......@@ -21,7 +21,7 @@
* Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen.
*/
import { isEscapeKey, isNotNull } from '@alfa-client/tech-shared';
import { isEscapeKey } from '@alfa-client/tech-shared';
import { InteractivityChecker } from '@angular/cdk/a11y';
import {
AfterViewInit,
......@@ -35,10 +35,15 @@ import {
Renderer2,
ViewContainerRef,
} from '@angular/core';
import { isNull, uniqueId } from 'lodash-es';
import { isEmpty, isNull, uniqueId } from 'lodash-es';
import { TooltipComponent } from './tooltip.component';
export enum TooltipPosition {
ABOVE = 'above',
BELOW = 'below',
}
const OUTLINE_INDENT = 4; // Outline offset (2) + outline width (2)
type TooltipAriaType = 'aria-describedby' | 'aria-labelledby';
@Directive({
selector: '[tooltip]',
......@@ -46,10 +51,15 @@ const OUTLINE_INDENT = 4; // Outline offset (2) + outline width (2)
})
export class TooltipDirective implements AfterViewInit, OnDestroy {
@Input() tooltip: string = '';
@Input() tooltipPosition: TooltipPosition = TooltipPosition.BELOW;
@Input() tooltipAriaType: TooltipAriaType = 'aria-describedby';
componentRef: ComponentRef<TooltipComponent> = null;
focusableElement: HTMLElement = null;
parentElement: HTMLElement = null;
tooltipId: string;
attachedToFocused: boolean = false;
position: TooltipPosition;
leftOffset: number = 0;
public viewContainerRef: ViewContainerRef = inject(ViewContainerRef);
public elementRef: ElementRef<HTMLElement> = inject(ElementRef);
......@@ -68,8 +78,8 @@ export class TooltipDirective implements AfterViewInit, OnDestroy {
@HostListener('focusin')
showTooltip(): void {
const nativeElement: HTMLElement = this.elementRef.nativeElement;
const attachedToFocused: boolean = nativeElement.contains(document.activeElement);
this.setTooltipProperties(attachedToFocused);
this.attachedToFocused = nativeElement.contains(document.activeElement);
this.setTooltipProperties();
}
@HostListener('mouseleave')
......@@ -88,49 +98,107 @@ export class TooltipDirective implements AfterViewInit, OnDestroy {
}
createTooltip(): void {
if (isNotNull(this.componentRef)) {
if (isEmpty(this.tooltip)) {
return;
}
const nativeElement: HTMLElement = this.elementRef.nativeElement;
this.componentRef = this.viewContainerRef.createComponent(TooltipComponent);
this.focusableElement =
this.interactivityChecker.isFocusable(nativeElement) ? nativeElement : this.getFocusableElement(nativeElement);
this.focusableElement.appendChild(this.componentRef.location.nativeElement);
this.setAriaLabeledBy();
this.parentElement = this.getParentElement(nativeElement);
this.parentElement.appendChild(this.componentRef.location.nativeElement);
this.tooltipId = uniqueId('tooltip');
this.setInitialTooltipProperties(this.tooltip, this.tooltipId);
this.setAriaAttribute(this.tooltipAriaType);
}
setTooltipProperties(attachedToFocused = false): void {
if (isNull(this.componentRef)) {
return;
setInitialTooltipProperties(text: string, id: string) {
this.componentRef.instance.text = text;
this.componentRef.instance.id = id;
}
const { left, right, bottom } = this.elementRef.nativeElement.getBoundingClientRect();
this.componentRef.instance.left = (right + left) / 2;
this.componentRef.instance.top = attachedToFocused ? bottom + OUTLINE_INDENT : bottom;
this.componentRef.instance.text = this.tooltip;
this.componentRef.instance.id = this.tooltipId;
setTooltipProperties(): void {
const { left, right, top, bottom } = this.elementRef.nativeElement.getBoundingClientRect();
this.setTooltipOffsetAndPosition();
this.componentRef.instance.left = (right + left) / 2 + this.leftOffset;
this.componentRef.instance.top = this.getTopPosition(top, bottom, this.position, this.attachedToFocused);
this.componentRef.instance.position = this.position;
this.componentRef.instance.leftOffset = this.leftOffset;
this.componentRef.instance.show = true;
}
setAriaLabeledBy(): void {
this.tooltipId = uniqueId('tooltip');
this.renderer.setAttribute(this.focusableElement, 'aria-labeledby', this.tooltipId);
setAutoPosition(tooltipHeight: number, parentRect: DOMRect, windowHeight: number): void {
const { top, bottom } = parentRect;
if (tooltipHeight > windowHeight - bottom) {
this.position = TooltipPosition.ABOVE;
return;
}
removeAriaLabeledBy(): void {
this.renderer.removeAttribute(this.focusableElement, 'aria-labeledby');
if (tooltipHeight > top) {
this.position = TooltipPosition.BELOW;
return;
}
this.position = this.tooltipPosition;
}
setLeftOffset(tooltipWidth: number, parentRect: DOMRect, windowWidth: number): void {
const { left, right, width } = parentRect;
const halfTooltipWidth: number = tooltipWidth / 2;
if (tooltipWidth < width) {
this.leftOffset = 0;
return;
}
if (halfTooltipWidth > left) {
this.leftOffset = halfTooltipWidth - left;
return;
}
if (halfTooltipWidth > windowWidth - right) {
this.leftOffset = windowWidth - right - halfTooltipWidth;
return;
}
this.leftOffset = 0;
}
setTooltipOffsetAndPosition(): void {
const { width, height } = this.componentRef.location.nativeElement.children[0].getBoundingClientRect();
const parentRect: DOMRect = this.elementRef.nativeElement.getBoundingClientRect();
this.setLeftOffset(width, parentRect, window.innerWidth);
this.setAutoPosition(height, parentRect, window.innerHeight);
}
setAriaAttribute(ariaType: TooltipAriaType): void {
this.renderer.setAttribute(this.parentElement, ariaType, this.tooltipId);
}
removeAriaAttribute(ariaType: TooltipAriaType): void {
this.renderer.removeAttribute(this.parentElement, ariaType);
}
getParentElement(element: HTMLElement): HTMLElement {
if (this.interactivityChecker.isFocusable(element)) {
return element;
}
return this.getFocusableElement(element) ?? element;
}
getFocusableElement(element: HTMLElement): HTMLElement {
return element.querySelector('a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])');
}
hide(): void {
if (isNull(this.componentRef)) {
return;
getTopPosition(parentTop: number, parentBottom: number, tooltipPosition: TooltipPosition, attachedToFocused: boolean): number {
if (tooltipPosition === TooltipPosition.ABOVE) {
return parentTop - (attachedToFocused ? OUTLINE_INDENT : 0);
}
return parentBottom + (attachedToFocused ? OUTLINE_INDENT : 0);
}
hide(): void {
this.componentRef.instance.show = false;
}
......@@ -141,7 +209,7 @@ export class TooltipDirective implements AfterViewInit, OnDestroy {
this.componentRef.destroy();
this.componentRef = null;
this.removeAriaLabeledBy();
this.focusableElement = null;
this.removeAriaAttribute(this.tooltipAriaType);
this.parentElement = null;
}
}
......@@ -21,7 +21,7 @@
* Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen.
*/
import { moduleMetadata, type Meta, type StoryObj } from '@storybook/angular';
import { argsToTemplate, componentWrapperDecorator, moduleMetadata, type Meta, type StoryObj } from '@storybook/angular';
import { TooltipDirective } from './tooltip.directive';
const meta: Meta = {
......@@ -29,6 +29,7 @@ const meta: Meta = {
excludeStories: /.*Data$/,
tags: ['autodocs'],
decorators: [
componentWrapperDecorator((story) => `<div class="flex justify-between mt-16">${story}</div>`),
moduleMetadata({
imports: [TooltipDirective],
}),
......@@ -46,7 +47,37 @@ export default meta;
type Story = StoryObj;
export const Default: Story = {
args: { tooltip: 'I stay in the center', tooltipPosition: 'below', tooltipAriaType: 'aria-describedby' },
argTypes: {
tooltip: { description: 'Tooltip text' },
tooltipPosition: {
description: 'Tooltip position dependent on parent element',
control: 'select',
options: ['above', 'below'],
},
tooltipAriaType: {
description: 'If true sets aria-describedby attribute to parent element, otherwise aria-labeledby',
control: 'select',
options: ['aria-describedby', 'aria-labeledby'],
table: { defaultValue: { summary: 'aria-describedby' } },
},
},
render: (args) => ({
props: args,
template: `
<button tooltip="I am displayed to the right">→</button>
<button ${argsToTemplate(args)}>Hover me</button>
<button tooltip="I am displayed to the left">←</button>
`,
}),
};
export const Above: Story = {
render: () => ({
template: '<button tooltip="Hello">I have a tooltip!</button>',
template: `
<button tooltip="I am displayed to the right" tooltipPosition="above">→</button>
<button tooltip="I stay in the center" tooltipPosition="above">Hover me</button>
<button tooltip="I am displayed to the left" tooltipPosition="above">←</button>
`,
}),
};
......@@ -104,11 +104,7 @@
},
"useInferencePlugins": false,
"release": {
"projects": [
"admin",
"alfa",
"info"
],
"projects": ["admin", "alfa", "info"],
"projectsRelationship": "independent"
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment