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

OZG-7047 Add tests

* Use renderer to set attributes
parent 76e285eb
No related branches found
No related tags found
No related merge requests found
...@@ -3,14 +3,16 @@ import { Component } from '@angular/core'; ...@@ -3,14 +3,16 @@ import { Component } from '@angular/core';
@Component({ @Component({
selector: 'ods-tooltip', selector: 'ods-tooltip',
template: `<p template: `<p
class="fixed z-50 mt-2 -translate-x-1/2 animate-fadeIn 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-8 before:border-l-8 before:border-r-8 before:border-b-ozggray-900 before:border-l-transparent before:border-r-transparent before:content-[''] dark:bg-white dark:before:border-b-white" class="fixed z-50 mt-2 -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-8 before:border-l-8 before:border-r-8 before:border-b-ozggray-900 before:border-l-transparent before:border-r-transparent before:content-[''] dark:bg-white dark:before:border-b-white"
[style.left]="left + 'px'" [style.left]="left + 'px'"
[style.top]="top + 'px'" [style.top]="top + 'px'"
[attr.id]="id" [attr.id]="id"
role="tooltip"
> >
{{ text }} {{ text }}
</p>`, </p>`,
styles: [':host {@apply contents}'], styles: [':host {@apply contents}'],
standalone: true,
}) })
export class TooltipComponent { export class TooltipComponent {
text: string = ''; text: string = '';
......
import { ElementRef, ViewContainerRef } from '@angular/core'; import { InteractivityChecker } from '@angular/cdk/a11y';
import { ComponentRef, ElementRef, Renderer2, ViewContainerRef } from '@angular/core';
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { TooltipComponent } from './tooltip.component';
import { TooltipDirective } from './tooltip.directive'; import { TooltipDirective } from './tooltip.directive';
export class MockElementRef extends ElementRef {} class MockElementRef extends ElementRef {
nativeElement = {
contains: jest.fn(),
appendChild: jest.fn(),
};
}
describe('TooltipDirective', () => { describe('TooltipDirective', () => {
let directive: TooltipDirective;
const mockComponentRef: ComponentRef<TooltipComponent> = {
setInput: jest.fn(),
destroy: jest.fn(),
onDestroy: jest.fn(),
componentType: TooltipComponent,
changeDetectorRef: null,
location: null,
hostView: null,
injector: null,
instance: { id: '', left: 0, top: 0, text: '' },
};
beforeEach((): void => { beforeEach((): void => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ViewContainerRef, { provide: ElementRef, useClass: MockElementRef }], providers: [ViewContainerRef, { provide: ElementRef, useClass: MockElementRef }, Renderer2, InteractivityChecker],
});
}); });
it('should create an instance', () => {
TestBed.runInInjectionContext(() => { TestBed.runInInjectionContext(() => {
const directive: TooltipDirective = new TooltipDirective(); directive = new TooltipDirective();
});
});
it('should create a directive', () => {
expect(directive).toBeTruthy(); expect(directive).toBeTruthy();
}); });
});
describe('ngOnDestroy', () => { describe('ngOnDestroy', () => {
it('should destroy tooltip', () => { it('should destroy tooltip', () => {
TestBed.runInInjectionContext(() => {
const directive = new TooltipDirective();
directive.destroy = jest.fn(); directive.destroy = jest.fn();
directive.ngOnDestroy(); directive.ngOnDestroy();
...@@ -30,5 +47,175 @@ describe('TooltipDirective', () => { ...@@ -30,5 +47,175 @@ describe('TooltipDirective', () => {
expect(directive.destroy).toHaveBeenCalled(); expect(directive.destroy).toHaveBeenCalled();
}); });
}); });
describe('createTooltip', () => {
beforeEach(() => {
directive.viewContainerRef.createComponent = jest.fn().mockReturnValue({ location: { nativeElement: {} } });
directive.setDescribedBy = jest.fn();
directive.setTooltipProperties = jest.fn();
});
it('should create tooltip component', () => {
directive.createTooltip();
expect(directive.viewContainerRef.createComponent).toHaveBeenCalled();
});
it('should insert tooltip component to parent', () => {
directive.createTooltip();
expect(directive.elementRef.nativeElement.appendChild).toHaveBeenCalled();
});
it('should set aria described by attribute to parent', () => {
directive.createTooltip();
expect(directive.setDescribedBy).toHaveBeenCalled();
});
it('should set tooltip properties', () => {
directive.createTooltip();
expect(directive.setTooltipProperties).toHaveBeenCalled();
});
});
describe('destroyTooltip', () => {
it('should destroy tooltip', () => {
directive.destroy = jest.fn();
directive.destroyTooltip();
expect(directive.destroy).toHaveBeenCalled();
});
});
describe('onKeydown', () => {
it('should destroy tooltip if escape key pressed', () => {
directive.destroy = jest.fn();
const escapeEvent: KeyboardEvent = { ...new KeyboardEvent('esc'), key: 'Escape' };
directive.onKeydown(escapeEvent);
expect(directive.destroy).toHaveBeenCalled();
});
});
describe('setTooltipProperties', () => {
beforeEach(() => {
directive.componentRef = mockComponentRef;
directive.elementRef.nativeElement.getBoundingClientRect = jest
.fn()
.mockReturnValue({ left: 0, right: 1000, bottom: 1000 });
});
it('should get bounding client rect', () => {
directive.setTooltipProperties();
expect(directive.elementRef.nativeElement.getBoundingClientRect).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',
left: 500,
text: 'I am tooltip',
top: 1000,
});
});
it('should add margin if parent element focused', () => {
directive.setTooltipProperties(true);
expect(directive.componentRef.instance.top).toBe(1004);
});
});
describe('setDescribedBy', () => {
beforeEach(() => {
directive.getFocusableElement = jest.fn();
directive.renderer.setAttribute = jest.fn();
directive.interactivityChecker.isFocusable = jest.fn();
});
it('should check if parent element focusable', () => {
directive.setDescribedBy();
expect(directive.interactivityChecker.isFocusable).toHaveBeenCalled();
});
it('should get focusable element if parent not focusable', () => {
directive.setDescribedBy();
expect(directive.getFocusableElement).toHaveBeenCalled();
});
it('should set describedby attribute', () => {
directive.setDescribedBy();
expect(directive.renderer.setAttribute).toHaveBeenCalled();
});
});
describe('removeDescribedBy', () => {
beforeEach(() => {
directive.renderer.removeAttribute = jest.fn();
});
it('should remove describedby attribute', () => {
directive.removeDescribedBy();
expect(directive.renderer.removeAttribute).toHaveBeenCalled();
});
});
describe('getFocusableElement', () => {
it('should return null', () => {
const simpleElement = document.createElement('a');
const result: HTMLElement = directive.getFocusableElement(simpleElement);
expect(result).toBeNull();
});
it('should return focusable child element', () => {
const nestedElement = document.createElement('div');
const buttonElement = document.createElement('button');
nestedElement.appendChild(buttonElement);
const result: HTMLElement = directive.getFocusableElement(nestedElement);
expect(result).toBe(buttonElement);
});
});
describe('destroy', () => {
beforeEach(() => {
directive.componentRef = mockComponentRef;
directive.removeDescribedBy = jest.fn();
});
it('should set component ref to null', () => {
directive.destroy();
expect(directive.componentRef).toBeNull();
});
it('should remove describedby attribute', () => {
directive.destroy();
expect(directive.removeDescribedBy).toHaveBeenCalled();
});
it('should set focusable element to null', () => {
directive.destroy();
expect(directive.focusableElement).toBeNull();
});
}); });
}); });
import { isEscapeKey } from '@alfa-client/tech-shared';
import { InteractivityChecker } from '@angular/cdk/a11y';
import { import {
ComponentRef, ComponentRef,
Directive, Directive,
ElementRef, ElementRef,
HostBinding,
HostListener, HostListener,
inject, inject,
Input, Input,
OnDestroy, OnDestroy,
Renderer2,
ViewContainerRef, ViewContainerRef,
} from '@angular/core'; } from '@angular/core';
import { uniqueId } from 'lodash-es'; import { uniqueId } from 'lodash-es';
...@@ -19,27 +21,28 @@ import { TooltipComponent } from './tooltip.component'; ...@@ -19,27 +21,28 @@ import { TooltipComponent } from './tooltip.component';
export class TooltipDirective implements OnDestroy { export class TooltipDirective implements OnDestroy {
@Input() tooltip: string = ''; @Input() tooltip: string = '';
private componentRef: ComponentRef<TooltipComponent> = null; componentRef: ComponentRef<TooltipComponent> = null;
private tooltipId: string; focusableElement: HTMLElement = null;
public readonly viewContainerRef: ViewContainerRef = inject(ViewContainerRef); tooltipId: string;
public readonly elementRef: ElementRef<HTMLElement> = inject(ElementRef);
public viewContainerRef: ViewContainerRef = inject(ViewContainerRef);
public elementRef: ElementRef<HTMLElement> = inject(ElementRef);
public renderer: Renderer2 = inject(Renderer2);
public interactivityChecker: InteractivityChecker = inject(InteractivityChecker);
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroy(); this.destroy();
} }
@HostBinding('attr.aria-describedby') get describedBy() {
return this.tooltipId;
}
@HostListener('mouseenter') @HostListener('mouseenter')
@HostListener('focusin') @HostListener('focusin')
createTooltip(): void { createTooltip(): void {
if (this.componentRef === null) { if (this.componentRef === null) {
const attachedToFocused: boolean = this.elementRef.nativeElement.contains(document.activeElement); const nativeElement: HTMLElement = this.elementRef.nativeElement;
this.tooltipId = uniqueId('tooltip'); const attachedToFocused: boolean = nativeElement.contains(document.activeElement);
this.componentRef = this.viewContainerRef.createComponent(TooltipComponent); this.componentRef = this.viewContainerRef.createComponent(TooltipComponent);
this.viewContainerRef.insert(this.componentRef.hostView); nativeElement.appendChild(this.componentRef.location.nativeElement);
this.setDescribedBy();
this.setTooltipProperties(attachedToFocused); this.setTooltipProperties(attachedToFocused);
} }
} }
...@@ -51,8 +54,9 @@ export class TooltipDirective implements OnDestroy { ...@@ -51,8 +54,9 @@ export class TooltipDirective implements OnDestroy {
this.destroy(); this.destroy();
} }
@HostListener('keydown', ['$event']) onKeydown(e: KeyboardEvent) { @HostListener('keydown', ['$event'])
if (this.isEscapeKey(e)) { onKeydown(e: KeyboardEvent): void {
if (isEscapeKey(e)) {
this.destroy(); this.destroy();
} }
} }
...@@ -67,15 +71,28 @@ export class TooltipDirective implements OnDestroy { ...@@ -67,15 +71,28 @@ export class TooltipDirective implements OnDestroy {
} }
} }
setDescribedBy(): void {
const nativeElement: HTMLElement = this.elementRef.nativeElement;
this.tooltipId = uniqueId('tooltip');
this.focusableElement =
this.interactivityChecker.isFocusable(nativeElement) ? nativeElement : this.getFocusableElement(nativeElement);
this.renderer.setAttribute(this.focusableElement, 'aria-describedby', this.tooltipId);
}
removeDescribedBy(): void {
this.renderer.removeAttribute(this.focusableElement, 'aria-describedby');
}
getFocusableElement(element: HTMLElement): HTMLElement {
return element.querySelector('a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])');
}
destroy(): void { destroy(): void {
if (this.componentRef !== null) { if (this.componentRef !== null) {
this.componentRef.destroy(); this.componentRef.destroy();
this.componentRef = null; this.componentRef = null;
this.tooltipId = null; this.removeDescribedBy();
} this.focusableElement = null;
} }
isEscapeKey(e: KeyboardEvent): boolean {
return e.key === 'Escape';
} }
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment