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

OZG-7367 Add described by variant

* Attach tooltip not only to focusable element but to not focusable too
parent d2d4269e
No related branches found
No related tags found
1 merge request!2OZG-7367 Extend tooltip directive
...@@ -45,6 +45,7 @@ ...@@ -45,6 +45,7 @@
href="#" 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" 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" tooltip="This is tooltip attached to link element"
tooltipDescribed="true"
> >
<div class="flex-1 basis-5/6"> <div class="flex-1 basis-5/6">
<div class="flex flex-wrap items-center gap-x-3"> <div class="flex flex-wrap items-center gap-x-3">
...@@ -422,7 +423,7 @@ ...@@ -422,7 +423,7 @@
</form> </form>
<app-bescheid-dialog-button></app-bescheid-dialog-button> <app-bescheid-dialog-button></app-bescheid-dialog-button>
<div class="my-4 flex gap-4"> <div class="my-4 flex gap-4">
<ods-button text="Button 1" tooltip="Sample tooltip" /> <ods-button text="Button 1" tooltip="Sample tooltip" tooltipDescribed="true" />
<ods-button size="medium" [isLoading]="true" text="Button 2" /> <ods-button size="medium" [isLoading]="true" text="Button 2" />
<ods-button type="outline" text="Button 3" /> <ods-button type="outline" text="Button 3" />
</div> </div>
......
...@@ -84,8 +84,8 @@ describe('TooltipDirective', () => { ...@@ -84,8 +84,8 @@ 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.setAriaLabeledBy = jest.fn(); directive.setAriaAttribute = jest.fn();
directive.getFocusableElement = jest.fn().mockReturnValue({ appendChild: jest.fn() }); directive.getParentElement = jest.fn().mockReturnValue({ appendChild: jest.fn() });
directive.interactivityChecker.isFocusable = jest.fn(); directive.interactivityChecker.isFocusable = jest.fn();
}); });
...@@ -95,28 +95,22 @@ describe('TooltipDirective', () => { ...@@ -95,28 +95,22 @@ describe('TooltipDirective', () => {
expect(directive.viewContainerRef.createComponent).toHaveBeenCalled(); expect(directive.viewContainerRef.createComponent).toHaveBeenCalled();
}); });
it('should check if parent element focusable', () => { it('should get parent element', () => {
directive.createTooltip(); directive.createTooltip();
expect(directive.interactivityChecker.isFocusable).toHaveBeenCalled(); expect(directive.getParentElement).toHaveBeenCalled();
});
it('should get focusable element if parent not focusable', () => {
directive.createTooltip();
expect(directive.getFocusableElement).toHaveBeenCalled();
}); });
it('should insert tooltip component to focusable', () => { it('should insert tooltip component to parent element', () => {
directive.createTooltip(); directive.createTooltip();
expect(directive.focusableElement.appendChild).toHaveBeenCalled(); expect(directive.parentElement.appendChild).toHaveBeenCalled();
}); });
it('should set aria-labeledby attribute to parent', () => { it('should set aria attribute to parent', () => {
directive.createTooltip(); directive.createTooltip();
expect(directive.setAriaLabeledBy).toHaveBeenCalled(); expect(directive.setAriaAttribute).toHaveBeenCalled();
}); });
}); });
...@@ -196,27 +190,93 @@ describe('TooltipDirective', () => { ...@@ -196,27 +190,93 @@ describe('TooltipDirective', () => {
}); });
}); });
describe('setAriaLabeledBy', () => { describe('setAriaAttribute', () => {
beforeEach(() => { beforeEach(() => {
directive.renderer.setAttribute = jest.fn(); directive.renderer.setAttribute = jest.fn();
directive.parentElement = null;
directive.tooltipId = 'test';
});
it('should set aria-describedby attribute', () => {
directive.setAriaAttribute(true);
expect(directive.renderer.setAttribute).toHaveBeenCalledWith(null, 'aria-describedby', 'test');
}); });
it('should set aria-labeledby attribute', () => { it('should set aria-labeledby attribute', () => {
directive.setAriaLabeledBy(); directive.setAriaAttribute(false);
expect(directive.renderer.setAttribute).toHaveBeenCalled(); expect(directive.renderer.setAttribute).toHaveBeenCalledWith(null, 'aria-labeledby', 'test');
}); });
}); });
describe('removeAriaLabeledBy', () => { describe('removeAriaLabeledBy', () => {
beforeEach(() => { beforeEach(() => {
directive.renderer.removeAttribute = jest.fn(); directive.renderer.removeAttribute = jest.fn();
directive.parentElement = null;
});
it('should remove aria-describedby attribute', () => {
directive.removeAriaAttribute(true);
expect(directive.renderer.removeAttribute).toHaveBeenCalledWith(null, 'aria-describedby');
}); });
it('should remove aria-labeledby attribute', () => { it('should remove aria-labeledby attribute', () => {
directive.removeAriaLabeledBy(); directive.removeAriaAttribute(false);
expect(directive.renderer.removeAttribute).toHaveBeenCalledWith(null, 'aria-labeledby');
});
});
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 return element itself if focusable element is null', () => {
const element = document.createElement('div');
directive.getFocusableElement = jest.fn().mockReturnValue(null);
const result: HTMLElement = directive.getParentElement(element);
expect(directive.renderer.removeAttribute).toHaveBeenCalled(); expect(result).toBe(element);
}); });
}); });
...@@ -255,7 +315,7 @@ describe('TooltipDirective', () => { ...@@ -255,7 +315,7 @@ describe('TooltipDirective', () => {
describe('destroy', () => { describe('destroy', () => {
beforeEach(() => { beforeEach(() => {
directive.componentRef = mockComponentRef; directive.componentRef = mockComponentRef;
directive.removeAriaLabeledBy = jest.fn(); directive.removeAriaAttribute = jest.fn();
}); });
it('should set component ref to null', () => { it('should set component ref to null', () => {
...@@ -267,13 +327,13 @@ describe('TooltipDirective', () => { ...@@ -267,13 +327,13 @@ describe('TooltipDirective', () => {
it('should remove aria-labeledby attribute', () => { it('should remove aria-labeledby attribute', () => {
directive.destroy(); directive.destroy();
expect(directive.removeAriaLabeledBy).toHaveBeenCalled(); expect(directive.removeAriaAttribute).toHaveBeenCalled();
}); });
it('should set focusable element to null', () => { it('should set focusable element to null', () => {
directive.destroy(); directive.destroy();
expect(directive.focusableElement).toBeNull(); expect(directive.parentElement).toBeNull();
}); });
}); });
}); });
...@@ -46,9 +46,10 @@ const OUTLINE_INDENT = 4; // Outline offset (2) + outline width (2) ...@@ -46,9 +46,10 @@ const OUTLINE_INDENT = 4; // Outline offset (2) + outline width (2)
}) })
export class TooltipDirective implements AfterViewInit, OnDestroy { export class TooltipDirective implements AfterViewInit, OnDestroy {
@Input() tooltip: string = ''; @Input() tooltip: string = '';
@Input() tooltipDescribed: boolean = false;
componentRef: ComponentRef<TooltipComponent> = null; componentRef: ComponentRef<TooltipComponent> = null;
focusableElement: HTMLElement = null; parentElement: HTMLElement = null;
tooltipId: string; tooltipId: string;
public viewContainerRef: ViewContainerRef = inject(ViewContainerRef); public viewContainerRef: ViewContainerRef = inject(ViewContainerRef);
...@@ -94,10 +95,10 @@ export class TooltipDirective implements AfterViewInit, OnDestroy { ...@@ -94,10 +95,10 @@ export class TooltipDirective implements AfterViewInit, OnDestroy {
const nativeElement: HTMLElement = this.elementRef.nativeElement; const nativeElement: HTMLElement = this.elementRef.nativeElement;
this.componentRef = this.viewContainerRef.createComponent(TooltipComponent); this.componentRef = this.viewContainerRef.createComponent(TooltipComponent);
this.focusableElement = this.parentElement = this.getParentElement(nativeElement);
this.interactivityChecker.isFocusable(nativeElement) ? nativeElement : this.getFocusableElement(nativeElement); this.parentElement.appendChild(this.componentRef.location.nativeElement);
this.focusableElement.appendChild(this.componentRef.location.nativeElement); this.tooltipId = uniqueId('tooltip');
this.setAriaLabeledBy(); this.setAriaAttribute(this.tooltipDescribed);
} }
setTooltipProperties(attachedToFocused = false): void { setTooltipProperties(attachedToFocused = false): void {
...@@ -113,13 +114,19 @@ export class TooltipDirective implements AfterViewInit, OnDestroy { ...@@ -113,13 +114,19 @@ export class TooltipDirective implements AfterViewInit, OnDestroy {
this.componentRef.instance.show = true; this.componentRef.instance.show = true;
} }
setAriaLabeledBy(): void { setAriaAttribute(describedBy: boolean): void {
this.tooltipId = uniqueId('tooltip'); this.renderer.setAttribute(this.parentElement, describedBy ? 'aria-describedby' : 'aria-labeledby', this.tooltipId);
this.renderer.setAttribute(this.focusableElement, 'aria-labeledby', this.tooltipId);
} }
removeAriaLabeledBy(): void { removeAriaAttribute(describedBy: boolean): void {
this.renderer.removeAttribute(this.focusableElement, 'aria-labeledby'); this.renderer.removeAttribute(this.parentElement, describedBy ? 'aria-describedby' : 'aria-labeledby');
}
getParentElement(element: HTMLElement): HTMLElement {
if (this.interactivityChecker.isFocusable(element)) {
return element;
}
return this.getFocusableElement(element) ?? element;
} }
getFocusableElement(element: HTMLElement): HTMLElement { getFocusableElement(element: HTMLElement): HTMLElement {
...@@ -141,7 +148,7 @@ export class TooltipDirective implements AfterViewInit, OnDestroy { ...@@ -141,7 +148,7 @@ export class TooltipDirective implements AfterViewInit, OnDestroy {
this.componentRef.destroy(); this.componentRef.destroy();
this.componentRef = null; this.componentRef = null;
this.removeAriaLabeledBy(); this.removeAriaAttribute(this.tooltipDescribed);
this.focusableElement = null; this.parentElement = null;
} }
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment