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

Merge branch 'master' into OZG-7078-bugfix

parents 8de3bbab 2466072c
Branches
Tags
No related merge requests found
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LinkComponent, TooltipDirective } from '@ods/system';
import { MockComponent, MockDirective } from 'ng-mocks';
import { AccessibilityButtonComponent } from './accessibility-button.component'; import { AccessibilityButtonComponent } from './accessibility-button.component';
describe('AccessibilityButtonComponent', () => { describe('AccessibilityButtonComponent', () => {
...@@ -7,7 +9,7 @@ describe('AccessibilityButtonComponent', () => { ...@@ -7,7 +9,7 @@ describe('AccessibilityButtonComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [AccessibilityButtonComponent], imports: [AccessibilityButtonComponent, MockComponent(LinkComponent), MockDirective(TooltipDirective)],
}).compileComponents(); }).compileComponents();
fixture = TestBed.createComponent(AccessibilityButtonComponent); fixture = TestBed.createComponent(AccessibilityButtonComponent);
......
...@@ -26,7 +26,9 @@ import { Component } from '@angular/core'; ...@@ -26,7 +26,9 @@ import { Component } from '@angular/core';
@Component({ @Component({
selector: 'ods-tooltip', selector: 'ods-tooltip',
template: `<p template: `<p
class="fixed z-[100] 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-[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="fixed hidden z-[100] 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-[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.left]="left + 'px'"
[style.top]="top + 'px'" [style.top]="top + 'px'"
[attr.id]="id" [attr.id]="id"
...@@ -42,5 +44,6 @@ export class TooltipComponent { ...@@ -42,5 +44,6 @@ export class TooltipComponent {
text: string = ''; text: string = '';
left: number = 0; left: number = 0;
top: number = 0; top: number = 0;
show: boolean = false;
id: string; id: string;
} }
...@@ -45,7 +45,7 @@ describe('TooltipDirective', () => { ...@@ -45,7 +45,7 @@ describe('TooltipDirective', () => {
location: null, location: null,
hostView: null, hostView: null,
injector: null, injector: null,
instance: { id: '', left: 0, top: 0, text: '' }, instance: { id: '', left: 0, top: 0, text: '', show: false },
}; };
beforeEach((): void => { beforeEach((): void => {
...@@ -74,8 +74,7 @@ describe('TooltipDirective', () => { ...@@ -74,8 +74,7 @@ describe('TooltipDirective', () => {
describe('createTooltip', () => { describe('createTooltip', () => {
beforeEach(() => { beforeEach(() => {
directive.viewContainerRef.createComponent = jest.fn().mockReturnValue({ location: { nativeElement: {} } }); directive.viewContainerRef.createComponent = jest.fn().mockReturnValue({ location: { nativeElement: {} } });
directive.setAriaDescribedBy = jest.fn(); directive.setAriaLabeledBy = jest.fn();
directive.setTooltipProperties = jest.fn();
}); });
it('should create tooltip component', () => { it('should create tooltip component', () => {
...@@ -90,37 +89,50 @@ describe('TooltipDirective', () => { ...@@ -90,37 +89,50 @@ describe('TooltipDirective', () => {
expect(directive.elementRef.nativeElement.appendChild).toHaveBeenCalled(); expect(directive.elementRef.nativeElement.appendChild).toHaveBeenCalled();
}); });
it('should set aria-describedby attribute to parent', () => { it('should set aria-labeledby attribute to parent', () => {
directive.createTooltip(); directive.createTooltip();
expect(directive.setAriaDescribedBy).toHaveBeenCalled(); expect(directive.setAriaLabeledBy).toHaveBeenCalled();
});
});
describe('showTooltip', () => {
beforeEach(() => {
directive.setTooltipProperties = jest.fn();
directive.elementRef.nativeElement.contains = jest.fn().mockReturnValue(true);
});
it('should check if element focused', () => {
directive.showTooltip();
expect(directive.elementRef.nativeElement.contains).toHaveBeenCalled();
}); });
it('should set tooltip properties', () => { it('should set tooltip properties', () => {
directive.createTooltip(); directive.showTooltip();
expect(directive.setTooltipProperties).toHaveBeenCalled(); expect(directive.setTooltipProperties).toHaveBeenCalledWith(true);
}); });
}); });
describe('destroyTooltip', () => { describe('hideTooltip', () => {
it('should destroy tooltip', () => { it('should hide tooltip', () => {
directive.destroy = jest.fn(); directive.hide = jest.fn();
directive.destroyTooltip(); directive.hideTooltip();
expect(directive.destroy).toHaveBeenCalled(); expect(directive.hide).toHaveBeenCalled();
}); });
}); });
describe('onKeydown', () => { describe('onKeydown', () => {
it('should destroy tooltip if escape key pressed', () => { it('should hide tooltip if escape key pressed', () => {
directive.destroy = jest.fn(); directive.hide = jest.fn();
const escapeEvent: KeyboardEvent = { ...new KeyboardEvent('esc'), key: 'Escape' }; const escapeEvent: KeyboardEvent = { ...new KeyboardEvent('esc'), key: 'Escape' };
directive.onKeydown(escapeEvent); directive.onKeydown(escapeEvent);
expect(directive.destroy).toHaveBeenCalled(); expect(directive.hide).toHaveBeenCalled();
}); });
}); });
...@@ -149,6 +161,7 @@ describe('TooltipDirective', () => { ...@@ -149,6 +161,7 @@ describe('TooltipDirective', () => {
left: 500, left: 500,
text: 'I am tooltip', text: 'I am tooltip',
top: 1000, top: 1000,
show: true,
}); });
}); });
...@@ -159,7 +172,7 @@ describe('TooltipDirective', () => { ...@@ -159,7 +172,7 @@ describe('TooltipDirective', () => {
}); });
}); });
describe('setAriaDescribedBy', () => { describe('setAriaLabeledBy', () => {
beforeEach(() => { beforeEach(() => {
directive.getFocusableElement = jest.fn(); directive.getFocusableElement = jest.fn();
directive.renderer.setAttribute = jest.fn(); directive.renderer.setAttribute = jest.fn();
...@@ -167,31 +180,31 @@ describe('TooltipDirective', () => { ...@@ -167,31 +180,31 @@ describe('TooltipDirective', () => {
}); });
it('should check if parent element focusable', () => { it('should check if parent element focusable', () => {
directive.setAriaDescribedBy(); directive.setAriaLabeledBy();
expect(directive.interactivityChecker.isFocusable).toHaveBeenCalled(); expect(directive.interactivityChecker.isFocusable).toHaveBeenCalled();
}); });
it('should get focusable element if parent not focusable', () => { it('should get focusable element if parent not focusable', () => {
directive.setAriaDescribedBy(); directive.setAriaLabeledBy();
expect(directive.getFocusableElement).toHaveBeenCalled(); expect(directive.getFocusableElement).toHaveBeenCalled();
}); });
it('should set aria-describedby attribute', () => { it('should set aria-labeledby attribute', () => {
directive.setAriaDescribedBy(); directive.setAriaLabeledBy();
expect(directive.renderer.setAttribute).toHaveBeenCalled(); expect(directive.renderer.setAttribute).toHaveBeenCalled();
}); });
}); });
describe('removeAriaDescribedBy', () => { describe('removeAriaLabeledBy', () => {
beforeEach(() => { beforeEach(() => {
directive.renderer.removeAttribute = jest.fn(); directive.renderer.removeAttribute = jest.fn();
}); });
it('should remove aria-describedby attribute', () => { it('should remove aria-labeledby attribute', () => {
directive.removeAriaDescribedBy(); directive.removeAriaLabeledBy();
expect(directive.renderer.removeAttribute).toHaveBeenCalled(); expect(directive.renderer.removeAttribute).toHaveBeenCalled();
}); });
...@@ -217,10 +230,22 @@ describe('TooltipDirective', () => { ...@@ -217,10 +230,22 @@ describe('TooltipDirective', () => {
}); });
}); });
describe('hide', () => {
beforeEach(() => {
directive.componentRef = Object.assign(mockComponentRef, { instance: { ...mockComponentRef.instance, show: true } });
});
it('should hide component', () => {
directive.hide();
expect(directive.componentRef.instance.show).toBeFalsy;
});
});
describe('destroy', () => { describe('destroy', () => {
beforeEach(() => { beforeEach(() => {
directive.componentRef = mockComponentRef; directive.componentRef = mockComponentRef;
directive.removeAriaDescribedBy = jest.fn(); directive.removeAriaLabeledBy = jest.fn();
}); });
it('should set component ref to null', () => { it('should set component ref to null', () => {
...@@ -229,10 +254,10 @@ describe('TooltipDirective', () => { ...@@ -229,10 +254,10 @@ describe('TooltipDirective', () => {
expect(directive.componentRef).toBeNull(); expect(directive.componentRef).toBeNull();
}); });
it('should remove aria-describedby attribute', () => { it('should remove aria-labeledby attribute', () => {
directive.destroy(); directive.destroy();
expect(directive.removeAriaDescribedBy).toHaveBeenCalled(); expect(directive.removeAriaLabeledBy).toHaveBeenCalled();
}); });
it('should set focusable element to null', () => { it('should set focusable element to null', () => {
......
...@@ -31,6 +31,7 @@ import { ...@@ -31,6 +31,7 @@ import {
inject, inject,
Input, Input,
OnDestroy, OnDestroy,
OnInit,
Renderer2, Renderer2,
ViewContainerRef, ViewContainerRef,
} from '@angular/core'; } from '@angular/core';
...@@ -43,7 +44,7 @@ const OUTLINE_INDENT = 4; // Outline offset (2) + outline width (2) ...@@ -43,7 +44,7 @@ const OUTLINE_INDENT = 4; // Outline offset (2) + outline width (2)
selector: '[tooltip]', selector: '[tooltip]',
standalone: true, standalone: true,
}) })
export class TooltipDirective implements OnDestroy { export class TooltipDirective implements OnInit, OnDestroy {
@Input() tooltip: string = ''; @Input() tooltip: string = '';
componentRef: ComponentRef<TooltipComponent> = null; componentRef: ComponentRef<TooltipComponent> = null;
...@@ -55,22 +56,19 @@ export class TooltipDirective implements OnDestroy { ...@@ -55,22 +56,19 @@ export class TooltipDirective implements OnDestroy {
public renderer: Renderer2 = inject(Renderer2); public renderer: Renderer2 = inject(Renderer2);
public interactivityChecker: InteractivityChecker = inject(InteractivityChecker); public interactivityChecker: InteractivityChecker = inject(InteractivityChecker);
ngOnInit(): void {
this.createTooltip();
}
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroy(); this.destroy();
} }
@HostListener('mouseenter') @HostListener('mouseenter')
@HostListener('focusin') @HostListener('focusin')
createTooltip(): void { showTooltip(): void {
if (isNotNull(this.componentRef)) {
return;
}
const nativeElement: HTMLElement = this.elementRef.nativeElement; const nativeElement: HTMLElement = this.elementRef.nativeElement;
const attachedToFocused: boolean = nativeElement.contains(document.activeElement); const attachedToFocused: boolean = nativeElement.contains(document.activeElement);
this.componentRef = this.viewContainerRef.createComponent(TooltipComponent);
nativeElement.appendChild(this.componentRef.location.nativeElement);
this.setAriaDescribedBy();
this.setTooltipProperties(attachedToFocused); this.setTooltipProperties(attachedToFocused);
} }
...@@ -78,17 +76,28 @@ export class TooltipDirective implements OnDestroy { ...@@ -78,17 +76,28 @@ export class TooltipDirective implements OnDestroy {
@HostListener('window:scroll') @HostListener('window:scroll')
@HostListener('focusout') @HostListener('focusout')
@HostListener('window:resize') @HostListener('window:resize')
destroyTooltip(): void { hideTooltip(): void {
this.destroy(); this.hide();
} }
@HostListener('keydown', ['$event']) @HostListener('keydown', ['$event'])
onKeydown(e: KeyboardEvent): void { onKeydown(e: KeyboardEvent): void {
if (isEscapeKey(e)) { if (isEscapeKey(e)) {
this.destroy(); this.hide();
} }
} }
createTooltip(): void {
if (isNotNull(this.componentRef)) {
return;
}
const nativeElement: HTMLElement = this.elementRef.nativeElement;
this.componentRef = this.viewContainerRef.createComponent(TooltipComponent);
nativeElement.appendChild(this.componentRef.location.nativeElement);
this.setAriaLabeledBy();
}
setTooltipProperties(attachedToFocused = false): void { setTooltipProperties(attachedToFocused = false): void {
if (isNull(this.componentRef)) { if (isNull(this.componentRef)) {
return; return;
...@@ -99,24 +108,33 @@ export class TooltipDirective implements OnDestroy { ...@@ -99,24 +108,33 @@ export class TooltipDirective implements OnDestroy {
this.componentRef.instance.top = attachedToFocused ? bottom + OUTLINE_INDENT : bottom; this.componentRef.instance.top = attachedToFocused ? bottom + OUTLINE_INDENT : bottom;
this.componentRef.instance.text = this.tooltip; this.componentRef.instance.text = this.tooltip;
this.componentRef.instance.id = this.tooltipId; this.componentRef.instance.id = this.tooltipId;
this.componentRef.instance.show = true;
} }
setAriaDescribedBy(): void { setAriaLabeledBy(): void {
const nativeElement: HTMLElement = this.elementRef.nativeElement; const nativeElement: HTMLElement = this.elementRef.nativeElement;
this.tooltipId = uniqueId('tooltip'); this.tooltipId = uniqueId('tooltip');
this.focusableElement = this.focusableElement =
this.interactivityChecker.isFocusable(nativeElement) ? nativeElement : this.getFocusableElement(nativeElement); this.interactivityChecker.isFocusable(nativeElement) ? nativeElement : this.getFocusableElement(nativeElement);
this.renderer.setAttribute(this.focusableElement, 'aria-describedby', this.tooltipId); this.renderer.setAttribute(this.focusableElement, 'aria-labeledby', this.tooltipId);
} }
removeAriaDescribedBy(): void { removeAriaLabeledBy(): void {
this.renderer.removeAttribute(this.focusableElement, 'aria-describedby'); this.renderer.removeAttribute(this.focusableElement, 'aria-labeledby');
} }
getFocusableElement(element: HTMLElement): HTMLElement { getFocusableElement(element: HTMLElement): HTMLElement {
return element.querySelector('a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])'); return element.querySelector('a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])');
} }
hide(): void {
if (isNull(this.componentRef)) {
return;
}
this.componentRef.instance.show = false;
}
destroy(): void { destroy(): void {
if (isNull(this.componentRef)) { if (isNull(this.componentRef)) {
return; return;
...@@ -124,7 +142,7 @@ export class TooltipDirective implements OnDestroy { ...@@ -124,7 +142,7 @@ export class TooltipDirective implements OnDestroy {
this.componentRef.destroy(); this.componentRef.destroy();
this.componentRef = null; this.componentRef = null;
this.removeAriaDescribedBy(); this.removeAriaLabeledBy();
this.focusableElement = null; this.focusableElement = null;
} }
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment