Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/bao tooltip #158

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
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