Skip to content
Open
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@ngxs/devtools-plugin": "^3.7.6",
"@ngxs/logger-plugin": "^3.7.6",
"@ngxs/store": "^3.7.6",
"@popperjs/core": "^2.11.6",
"@villedemontreal/hochelaga": "^4.23.1",
"rxjs": "~7.4.0",
"tslib": "^2.3.0",
Expand Down
2 changes: 2 additions & 0 deletions projects/angular-ui/src/lib/bao.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { BaoModalModule } from './modal/module';
import { BaoHyperlinkModule } from './hyperlink';
import { BaoDropdownMenuModule } from './dropdown-menu';
import { BaoFileModule } from './file/module';
import { BaoTooltipModule } from './tooltip';
import { BaoSnackBarModule } from './snack-bar/module';

@NgModule({
Expand Down Expand Up @@ -52,6 +53,7 @@ import { BaoSnackBarModule } from './snack-bar/module';
BaoHyperlinkModule,
BaoDropdownMenuModule,
BaoFileModule,
BaoTooltipModule,
BaoSnackBarModule
// TODO: reactivate once component does not depend on global css BaoBadgeModule,
]
Expand Down
7 changes: 7 additions & 0 deletions projects/angular-ui/src/lib/tooltip/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright (c) 2023 Ville de Montreal. All rights reserved.
* Licensed under the MIT license.
* See LICENSE file in the project root for full license information.
*/
export * from './module';
export * from './tooltip.directive';
16 changes: 16 additions & 0 deletions projects/angular-ui/src/lib/tooltip/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2023 Ville de Montreal. All rights reserved.
* Licensed under the MIT license.
* See LICENSE file in the project root for full license information.
*/
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { BaoTooltipComponent } from './tooltip.component';
import { BaoTooltipDirective } from './tooltip.directive';

@NgModule({
imports: [CommonModule],
declarations: [BaoTooltipComponent, BaoTooltipDirective],
exports: [BaoTooltipDirective]
})
export class BaoTooltipModule {}
Empty file.
27 changes: 27 additions & 0 deletions projects/angular-ui/src/lib/tooltip/tooltip.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@import '../core/colors';
@import '../core/typography';

.bao-tooltip {
position: absolute;
padding: 2px 8px;
max-width: 200px;
color: $neutral-primary-reversed;
background: $ground-reversed;
border-radius: 4px;
z-index: 1000;
visibility: hidden;
@include typo-interface-small;

&.bao-tooltip-show {
visibility: visible;
}
&.bao-tooltip-center {
text-align: center;
}
&.bao-tooltip-left {
text-align: left;
}
&.bao-tooltip-right {
text-align: right;
}
}
29 changes: 29 additions & 0 deletions projects/angular-ui/src/lib/tooltip/tooltip.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* tslint:disable:no-unused-variable */
import { ElementRef } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { BaoTooltipComponent } from './tooltip.component';

describe('TooltipPopperComponent', () => {
let component: BaoTooltipComponent;
let fixture: ComponentFixture<BaoTooltipComponent>;

beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [BaoTooltipComponent]
}).compileComponents();
})
);

beforeEach(() => {
fixture = TestBed.createComponent(BaoTooltipComponent);
component = fixture.componentInstance;
component.content = 'test content';
component.parentRef = new ElementRef(document.createElement('button'));
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
236 changes: 236 additions & 0 deletions projects/angular-ui/src/lib/tooltip/tooltip.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
/*
* Copyright (c) 2023 Ville de Montreal. All rights reserved.
* Licensed under the MIT license.
* See LICENSE file in the project root for full license information.
*/
import {
ChangeDetectionStrategy,
Component,
ElementRef,
Input,
OnInit,
Renderer2,
ViewEncapsulation,
} from '@angular/core';
import { BasePlacement, createPopper, Instance } from '@popperjs/core';
import {
isPlacement,
isTextAlign,
BaoTooltipPlacement,
BaoTooltipTextAlign,
} from './tooltip.model';

export interface IPos {
top: number;
left: number;
}
/**
* Unique ID for each tooltip counter
*/
let baoTooltipNextUniqueId = 0;

@Component({
selector: 'bao-tooltip',
templateUrl: 'tooltip.component.html',
styleUrls: ['./tooltip.component.scss'],
providers: [],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
class: 'bao-tooltip',
'[class.bao-tooltip-show]': 'show',
'[class.bao-tooltip-center]': 'textAlign==="center"',
'[class.bao-tooltip-left]': 'textAlign==="left"',
'[class.bao-tooltip-right]': 'textAlign==="right"',
},
})
export class BaoTooltipComponent implements OnInit {
/**
* The tooltip content
*/
@Input()
public content!: string;
/**
* The tooltip placement
*/
@Input()
public placement!: BaoTooltipPlacement;
/**
* The text alignement
*/
@Input()
public textAlign!: BaoTooltipTextAlign;
/**
* The parent node reference
*/
@Input()
public parentRef!: ElementRef;

offset = 10;
popperInstance!: Instance;
private _show = false;
private uniqueId = `bao-tooltip-${++baoTooltipNextUniqueId}`;

constructor(private renderer: Renderer2, private tooltipRef: ElementRef) {}

/**
* show or Hide tooltip
*/
@Input()
get show() {
return this._show;
}

set show(value: boolean) {
if (value !== this.show) {
this._show = value;
if (value) {
this.renderer.setAttribute(
this.tooltipRef.nativeElement,
'aria-hidden',
'false'
);
} else {
this.renderer.setAttribute(
this.tooltipRef.nativeElement,
'aria-hidden',
'true'
);
}
}
}

ngOnInit() {
this.placement = this.getPlacementValid(this.placement);
this.textAlign = this.getTextAlignValid(this.textAlign);
this.create();
}

destroy() {
this.popperInstance.destroy();
}

/**
* Valid the input placement an return a valid value (default top)
*/
private getPlacementValid(
placement: BaoTooltipPlacement
): BaoTooltipPlacement {
if (isPlacement(placement)) {
return placement;
}
return 'top';
}

/**
* Valid the input textAlign an return a valid value (default center)
*/
private getTextAlignValid(
textAlign: BaoTooltipTextAlign
): BaoTooltipTextAlign {
if (isTextAlign(textAlign)) {
return textAlign;
}
return 'center';
}

/**
* Prepare the content
* Set some attributes
* create the popper
*/
private create() {
this.cleanContentAndAddToTooltipRef();
// Set the aria-describedby attribute on parent element ref
this.renderer.setAttribute(
this.parentRef.nativeElement,
'aria-describedby',
this.uniqueId
);
// Set the id attribute on tooltip element ref
this.renderer.setAttribute(
this.tooltipRef.nativeElement,
'id',
this.uniqueId
);
// Set the initial aria-hidden attribute on tooltip element ref
this.renderer.setAttribute(
this.tooltipRef.nativeElement,
'aria-hidden',
'true'
);
// create the popper
this.popperInstance = createPopper(
this.parentRef.nativeElement,
this.tooltipRef.nativeElement,
{
placement: this.placement as BasePlacement,
modifiers: [
{
name: 'offset',
options: {
offset: [0, this.offset],
},
},
{
name: 'flip',
options: {
fallbackPlacements: this.getFallbackPlacements(
this.placement as BasePlacement
),
},
},
],
}
);
}

/**
* Clean the content (HTML content)
* Add content to tooltip ElementRef
*/
private cleanContentAndAddToTooltipRef() {
const cleanContent = this.removeNotAllowedTags(this.content);
const domParsed = new DOMParser();
const element = domParsed.parseFromString(
`<span>${cleanContent}</span>`,
'text/html'
).body.firstElementChild;
this.renderer.appendChild(this.tooltipRef.nativeElement, element);
}

/**
* Return the fallback Placements on overflow depending of the initial placement
*/
private getFallbackPlacements(initPlacement: BasePlacement): BasePlacement[] {
switch (initPlacement) {
case 'bottom':
return ['right', 'left', 'top'];
case 'right':
return ['left', 'top', 'bottom'];
case 'left':
return ['top', 'bottom', 'right'];
default:
return ['bottom', 'right', 'left'];
}
}

/**
* Remove all not allowed tags
* Allowed tags are : "div, b, i, u, p, ol, ul, li, br"
*/
private removeNotAllowedTags(input: string): string {
if (!input) return '';
const allowed = '<div><b><i><u><p><ol><ul><li><br>';
const allowedLowercase = (
((allowed || '') + '').toLowerCase().match(/<[a-z][a-z0-9]*>/g) || []
).join('');
const tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi;
const comments = /<!--[\s\S]*?-->/gi;
return input.replace(comments, '').replace(tags, function ($0, $1) {
return allowedLowercase.indexOf('<' + $1.toLowerCase() + '>') > -1
? $0
: '';
});
}
}
Loading