diff --git a/alfa-client/libs/common/src/lib/accessibility-button/accessibility-button.component.spec.ts b/alfa-client/libs/common/src/lib/accessibility-button/accessibility-button.component.spec.ts index ad67dcf8ab0a9414481ab5db68b15534d2799751..8b150294c1af567bef737ca8909cae78eb9081fd 100644 --- a/alfa-client/libs/common/src/lib/accessibility-button/accessibility-button.component.spec.ts +++ b/alfa-client/libs/common/src/lib/accessibility-button/accessibility-button.component.spec.ts @@ -1,4 +1,6 @@ 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'; describe('AccessibilityButtonComponent', () => { @@ -7,7 +9,7 @@ describe('AccessibilityButtonComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [AccessibilityButtonComponent], + imports: [AccessibilityButtonComponent, MockComponent(LinkComponent), MockDirective(TooltipDirective)], }).compileComponents(); fixture = TestBed.createComponent(AccessibilityButtonComponent); diff --git a/alfa-client/libs/design-system/src/lib/tooltip/tooltip.component.ts b/alfa-client/libs/design-system/src/lib/tooltip/tooltip.component.ts index 51b46d897d0ff01414a77fc37792fb24c1edd1b8..89697eccbb9f006612aa1063acd5a53627a8fefb 100644 --- a/alfa-client/libs/design-system/src/lib/tooltip/tooltip.component.ts +++ b/alfa-client/libs/design-system/src/lib/tooltip/tooltip.component.ts @@ -26,7 +26,9 @@ import { Component } from '@angular/core'; @Component({ selector: 'ods-tooltip', 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.top]="top + 'px'" [attr.id]="id" @@ -42,5 +44,6 @@ export class TooltipComponent { text: string = ''; left: number = 0; top: number = 0; + show: boolean = false; id: string; } diff --git a/alfa-client/libs/design-system/src/lib/tooltip/tooltip.directive.spec.ts b/alfa-client/libs/design-system/src/lib/tooltip/tooltip.directive.spec.ts index 25ae440cf0e5e90d427386862f00cd41abce9eb4..e39f39af4d4342b92c643cb8156c7c49ed77fcd2 100644 --- a/alfa-client/libs/design-system/src/lib/tooltip/tooltip.directive.spec.ts +++ b/alfa-client/libs/design-system/src/lib/tooltip/tooltip.directive.spec.ts @@ -45,7 +45,7 @@ describe('TooltipDirective', () => { location: null, hostView: null, injector: null, - instance: { id: '', left: 0, top: 0, text: '' }, + instance: { id: '', left: 0, top: 0, text: '', show: false }, }; beforeEach((): void => { @@ -74,8 +74,7 @@ describe('TooltipDirective', () => { describe('createTooltip', () => { beforeEach(() => { directive.viewContainerRef.createComponent = jest.fn().mockReturnValue({ location: { nativeElement: {} } }); - directive.setAriaDescribedBy = jest.fn(); - directive.setTooltipProperties = jest.fn(); + directive.setAriaLabeledBy = jest.fn(); }); it('should create tooltip component', () => { @@ -90,37 +89,50 @@ describe('TooltipDirective', () => { expect(directive.elementRef.nativeElement.appendChild).toHaveBeenCalled(); }); - it('should set aria-describedby attribute to parent', () => { + it('should set aria-labeledby attribute to parent', () => { 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', () => { - directive.createTooltip(); + directive.showTooltip(); - expect(directive.setTooltipProperties).toHaveBeenCalled(); + expect(directive.setTooltipProperties).toHaveBeenCalledWith(true); }); }); - describe('destroyTooltip', () => { - it('should destroy tooltip', () => { - directive.destroy = jest.fn(); + describe('hideTooltip', () => { + it('should hide tooltip', () => { + directive.hide = jest.fn(); - directive.destroyTooltip(); + directive.hideTooltip(); - expect(directive.destroy).toHaveBeenCalled(); + expect(directive.hide).toHaveBeenCalled(); }); }); describe('onKeydown', () => { - it('should destroy tooltip if escape key pressed', () => { - directive.destroy = jest.fn(); + it('should hide tooltip if escape key pressed', () => { + directive.hide = jest.fn(); const escapeEvent: KeyboardEvent = { ...new KeyboardEvent('esc'), key: 'Escape' }; directive.onKeydown(escapeEvent); - expect(directive.destroy).toHaveBeenCalled(); + expect(directive.hide).toHaveBeenCalled(); }); }); @@ -149,6 +161,7 @@ describe('TooltipDirective', () => { left: 500, text: 'I am tooltip', top: 1000, + show: true, }); }); @@ -159,7 +172,7 @@ describe('TooltipDirective', () => { }); }); - describe('setAriaDescribedBy', () => { + describe('setAriaLabeledBy', () => { beforeEach(() => { directive.getFocusableElement = jest.fn(); directive.renderer.setAttribute = jest.fn(); @@ -167,31 +180,31 @@ describe('TooltipDirective', () => { }); it('should check if parent element focusable', () => { - directive.setAriaDescribedBy(); + directive.setAriaLabeledBy(); expect(directive.interactivityChecker.isFocusable).toHaveBeenCalled(); }); it('should get focusable element if parent not focusable', () => { - directive.setAriaDescribedBy(); + directive.setAriaLabeledBy(); expect(directive.getFocusableElement).toHaveBeenCalled(); }); - it('should set aria-describedby attribute', () => { - directive.setAriaDescribedBy(); + it('should set aria-labeledby attribute', () => { + directive.setAriaLabeledBy(); expect(directive.renderer.setAttribute).toHaveBeenCalled(); }); }); - describe('removeAriaDescribedBy', () => { + describe('removeAriaLabeledBy', () => { beforeEach(() => { directive.renderer.removeAttribute = jest.fn(); }); - it('should remove aria-describedby attribute', () => { - directive.removeAriaDescribedBy(); + it('should remove aria-labeledby attribute', () => { + directive.removeAriaLabeledBy(); expect(directive.renderer.removeAttribute).toHaveBeenCalled(); }); @@ -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', () => { beforeEach(() => { directive.componentRef = mockComponentRef; - directive.removeAriaDescribedBy = jest.fn(); + directive.removeAriaLabeledBy = jest.fn(); }); it('should set component ref to null', () => { @@ -229,10 +254,10 @@ describe('TooltipDirective', () => { expect(directive.componentRef).toBeNull(); }); - it('should remove aria-describedby attribute', () => { + it('should remove aria-labeledby attribute', () => { directive.destroy(); - expect(directive.removeAriaDescribedBy).toHaveBeenCalled(); + expect(directive.removeAriaLabeledBy).toHaveBeenCalled(); }); it('should set focusable element to null', () => { diff --git a/alfa-client/libs/design-system/src/lib/tooltip/tooltip.directive.ts b/alfa-client/libs/design-system/src/lib/tooltip/tooltip.directive.ts index f624d583427930c3a5fbb8cf5c42d85c1c43f0bb..09d7958f93cd817c6cc8fba64f329e9684902c1c 100644 --- a/alfa-client/libs/design-system/src/lib/tooltip/tooltip.directive.ts +++ b/alfa-client/libs/design-system/src/lib/tooltip/tooltip.directive.ts @@ -31,6 +31,7 @@ import { inject, Input, OnDestroy, + OnInit, Renderer2, ViewContainerRef, } from '@angular/core'; @@ -43,7 +44,7 @@ const OUTLINE_INDENT = 4; // Outline offset (2) + outline width (2) selector: '[tooltip]', standalone: true, }) -export class TooltipDirective implements OnDestroy { +export class TooltipDirective implements OnInit, OnDestroy { @Input() tooltip: string = ''; componentRef: ComponentRef<TooltipComponent> = null; @@ -55,22 +56,19 @@ export class TooltipDirective implements OnDestroy { public renderer: Renderer2 = inject(Renderer2); public interactivityChecker: InteractivityChecker = inject(InteractivityChecker); + ngOnInit(): void { + this.createTooltip(); + } + ngOnDestroy(): void { this.destroy(); } @HostListener('mouseenter') @HostListener('focusin') - createTooltip(): void { - if (isNotNull(this.componentRef)) { - return; - } - + showTooltip(): void { const nativeElement: HTMLElement = this.elementRef.nativeElement; const attachedToFocused: boolean = nativeElement.contains(document.activeElement); - this.componentRef = this.viewContainerRef.createComponent(TooltipComponent); - nativeElement.appendChild(this.componentRef.location.nativeElement); - this.setAriaDescribedBy(); this.setTooltipProperties(attachedToFocused); } @@ -78,15 +76,26 @@ export class TooltipDirective implements OnDestroy { @HostListener('window:scroll') @HostListener('focusout') @HostListener('window:resize') - destroyTooltip(): void { - this.destroy(); + hideTooltip(): void { + this.hide(); } @HostListener('keydown', ['$event']) onKeydown(e: KeyboardEvent): void { 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 { @@ -99,24 +108,33 @@ export class TooltipDirective implements OnDestroy { this.componentRef.instance.top = attachedToFocused ? bottom + OUTLINE_INDENT : bottom; this.componentRef.instance.text = this.tooltip; this.componentRef.instance.id = this.tooltipId; + this.componentRef.instance.show = true; } - setAriaDescribedBy(): void { + setAriaLabeledBy(): 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); + this.renderer.setAttribute(this.focusableElement, 'aria-labeledby', this.tooltipId); } - removeAriaDescribedBy(): void { - this.renderer.removeAttribute(this.focusableElement, 'aria-describedby'); + removeAriaLabeledBy(): void { + this.renderer.removeAttribute(this.focusableElement, 'aria-labeledby'); } 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; + } + + this.componentRef.instance.show = false; + } + destroy(): void { if (isNull(this.componentRef)) { return; @@ -124,7 +142,7 @@ export class TooltipDirective implements OnDestroy { this.componentRef.destroy(); this.componentRef = null; - this.removeAriaDescribedBy(); + this.removeAriaLabeledBy(); this.focusableElement = null; } }