diff --git a/README.md b/README.md index 1e009b34e..ecad65bd3 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,12 @@ React version here: https://github.com/iurynogueira/ion-react +## Install in your project + +``` +npm i @brisanet/ion +``` + ## Install and run project To run this project, You will need to use [node v.12](https://nodejs.org/en/) diff --git a/projects/ion/package.json b/projects/ion/package.json index 59eb7e74d..307e77922 100644 --- a/projects/ion/package.json +++ b/projects/ion/package.json @@ -1,6 +1,6 @@ { "name": "@brisanet/ion", - "version": "0.0.72", + "version": "0.0.74", "repository": { "type": "git", "url": "git+https://github.com/Brisanet/ion.git" diff --git a/projects/ion/src/lib/sidebar/sidebar.component.ts b/projects/ion/src/lib/sidebar/sidebar.component.ts index ee1dbdaa4..eff85832f 100644 --- a/projects/ion/src/lib/sidebar/sidebar.component.ts +++ b/projects/ion/src/lib/sidebar/sidebar.component.ts @@ -15,22 +15,17 @@ export class IonSidebarComponent { public closed = true; public checkClikOnPageAccess = (event): void => { - this.checkClikOnPage(event); - }; - - public checkClikOnPage(event): void { - const containerElement = [document.querySelector('.ion-sidebar--opened')]; + const containerElement = document.querySelector('.ion-sidebar--opened'); const innerElement = event.target; - if (containerElement.length && !containerElement.includes(innerElement)) { + if (containerElement && !containerElement.contains(innerElement)) { const closeButton = document.querySelector( '.ion-sidebar--opened .ion-sidebar__header button' ) as HTMLElement; if (closeButton) { closeButton.click(); } - document.removeEventListener('click', this.checkClikOnPage); } - } + }; public toggleVisibility(): void { this.closed = !this.closed; @@ -38,7 +33,10 @@ export class IonSidebarComponent { setTimeout(() => { document.addEventListener('click', this.checkClikOnPageAccess); }); + + return; } + document.removeEventListener('click', this.checkClikOnPageAccess); } public itemSelected(itemIndex: number): void { diff --git a/projects/ion/src/lib/sidebar/sidebar.spec.ts b/projects/ion/src/lib/sidebar/sidebar.spec.ts index fc356bf00..912791556 100644 --- a/projects/ion/src/lib/sidebar/sidebar.spec.ts +++ b/projects/ion/src/lib/sidebar/sidebar.spec.ts @@ -215,20 +215,21 @@ describe('Sidebar', () => { it('should close sidebar after clicking outside it', async () => { jest.useFakeTimers(); const timeDelay = 300; - const { detectChanges } = await render(IonSidebarComponent, { + + await render(IonSidebarComponent, { declarations: [IonSidebarItemComponent, IonSidebarGroupComponent], imports: [CommonModule, IonIconModule, IonButtonModule], }); userEvent.click(getByTestId('toggleVisibility').firstElementChild); + jest.advanceTimersByTime(timeDelay); + expect(getByTestId('sidebar')).toHaveClass('ion-sidebar--opened'); userEvent.click( within(getByTestId('outsideContainer')).getByTestId( 'ion-sidebar__toggle-visibility' - ).firstElementChild + ) ); - jest.advanceTimersByTime(timeDelay); - detectChanges(); expect(getByTestId('sidebar')).not.toHaveClass('ion-sidebar--opened'); }); }); diff --git a/projects/ion/src/lib/smart-table/smart-table.component.html b/projects/ion/src/lib/smart-table/smart-table.component.html index 934144c44..a4f6477bf 100644 --- a/projects/ion/src/lib/smart-table/smart-table.component.html +++ b/projects/ion/src/lib/smart-table/smart-table.component.html @@ -206,7 +206,7 @@ [iconType]="action.icon" [circularButton]="true" [disabled]=" - action.disabled ? !disableAction(row, action) : false + action.disabled ? disableAction(row, action) : false " > diff --git a/projects/ion/src/lib/smart-table/smart-table.component.spec.ts b/projects/ion/src/lib/smart-table/smart-table.component.spec.ts index cf8221544..b34c7d640 100644 --- a/projects/ion/src/lib/smart-table/smart-table.component.spec.ts +++ b/projects/ion/src/lib/smart-table/smart-table.component.spec.ts @@ -168,7 +168,7 @@ describe('IonSmartTableComponent', () => { ); }); - it('should emit event sort with desc false when click in sort icon', async () => { + it('should emit event sort with desc true when click in sort icon', async () => { const orderBy = columns[0].key; fireEvent.click(screen.getByTestId('sort-by-' + orderBy)); expect(events).toHaveBeenCalledWith({ @@ -176,7 +176,7 @@ describe('IonSmartTableComponent', () => { event: EventTable.SORT, order: { column: orderBy, - desc: false, + desc: true, }, }); }); @@ -186,7 +186,7 @@ describe('IonSmartTableComponent', () => { fireEvent.click(screen.getByTestId('sort-by-' + orderBy)); expect(defaultProps.config.order).toStrictEqual({ column: orderBy, - desc: true, + desc: false, }); }); diff --git a/projects/ion/src/lib/smart-table/smart-table.component.ts b/projects/ion/src/lib/smart-table/smart-table.component.ts index cc57bdab0..d78947d94 100644 --- a/projects/ion/src/lib/smart-table/smart-table.component.ts +++ b/projects/ion/src/lib/smart-table/smart-table.component.ts @@ -103,6 +103,8 @@ export class IonSmartTableComponent implements OnInit, AfterViewChecked { } public sort(column: Column): void { + column.desc = !column.desc; + this.config.order = { column: column.key, desc: column.desc, @@ -112,7 +114,7 @@ export class IonSmartTableComponent implements OnInit, AfterViewChecked { change_page: this.pagination, order: { column: column.key, - desc: !!column.desc, + desc: column.desc, }, }); @@ -121,7 +123,6 @@ export class IonSmartTableComponent implements OnInit, AfterViewChecked { columnEach.desc = null; } }); - column.desc = !column.desc; } public handleEvent(row: SafeAny, action: ActionTable): void { diff --git a/projects/ion/src/lib/tooltip/tooltip.component.html b/projects/ion/src/lib/tooltip/tooltip.component.html index a85047dc5..bb2fea4eb 100644 --- a/projects/ion/src/lib/tooltip/tooltip.component.html +++ b/projects/ion/src/lib/tooltip/tooltip.component.html @@ -8,6 +8,7 @@ [class.ion-tooltip--visible]="ionTooltipVisible" [style.top]="top + 'px'" [style.left]="left + 'px'" + #tooltip > {{ ionTooltipTitle }} diff --git a/projects/ion/src/lib/tooltip/tooltip.component.spec.ts b/projects/ion/src/lib/tooltip/tooltip.component.spec.ts index 5f90490ac..078f33e97 100644 --- a/projects/ion/src/lib/tooltip/tooltip.component.spec.ts +++ b/projects/ion/src/lib/tooltip/tooltip.component.spec.ts @@ -1,12 +1,10 @@ import { CommonModule } from '@angular/common'; import { render, screen } from '@testing-library/angular'; -import { TooltipPosition, TooltipProps } from '../core/types'; +import { TooltipProps } from '../core/types'; import { IonTooltipComponent } from './tooltip.component'; const tooltipTestId = 'ion-tooltip'; -const positions = Object.values(TooltipPosition) as TooltipPosition[]; - const defaultProps: TooltipProps = { ionTooltipTitle: 'Title', }; @@ -38,15 +36,6 @@ describe('IonTooltipComponent', () => { await sut({ ionTooltipColorScheme: 'light' }); expect(screen.getByTestId(tooltipTestId)).toHaveClass('ion-tooltip-light'); }); - it.each(positions)( - 'should render tooltip on position %s', - async (position) => { - await sut({ ionTooltipPosition: position }); - expect(screen.getByTestId(tooltipTestId)).toHaveClass( - `ion-tooltip-position--${position}` - ); - } - ); it('should not have visible class when visibility is false', async () => { await sut(); expect(screen.getByTestId(tooltipTestId)).not.toHaveClass( diff --git a/projects/ion/src/lib/tooltip/tooltip.component.ts b/projects/ion/src/lib/tooltip/tooltip.component.ts index e9081d4f7..5fb9e3abf 100644 --- a/projects/ion/src/lib/tooltip/tooltip.component.ts +++ b/projects/ion/src/lib/tooltip/tooltip.component.ts @@ -1,12 +1,22 @@ -import { Component, TemplateRef } from '@angular/core'; +import { + AfterViewChecked, + ChangeDetectorRef, + Component, + ElementRef, + TemplateRef, + ViewChild, +} from '@angular/core'; import { TooltipColorScheme, TooltipPosition } from '../core/types'; +import { TooltipService } from './tooltip.service'; @Component({ selector: 'ion-tooltip', templateUrl: './tooltip.component.html', styleUrls: ['./tooltip.component.scss'], }) -export class IonTooltipComponent { +export class IonTooltipComponent implements AfterViewChecked { + @ViewChild('tooltip', { static: true }) tooltip: ElementRef; + ionTooltipTitle: string; ionTooltipTemplateRef: TemplateRef; ionTooltipColorScheme: TooltipColorScheme = 'dark'; @@ -14,4 +24,23 @@ export class IonTooltipComponent { ionTooltipVisible = false; left = 0; top = 0; + + constructor( + private cdr: ChangeDetectorRef, + private tooltipService: TooltipService + ) {} + + ngAfterViewChecked(): void { + this.repositionTooltip(); + this.cdr.detectChanges(); + } + + private repositionTooltip(): void { + const coordinates = this.tooltip.nativeElement.getBoundingClientRect(); + + this.tooltipService.setTooltipCoordinates(coordinates); + this.tooltipService.setCurrentPosition(this.ionTooltipPosition); + this.ionTooltipPosition = this.tooltipService.getNewPosition(); + this.tooltipService.emitReposition(); + } } diff --git a/projects/ion/src/lib/tooltip/tooltip.directive.spec.ts b/projects/ion/src/lib/tooltip/tooltip.directive.spec.ts index 65402040d..780825313 100644 --- a/projects/ion/src/lib/tooltip/tooltip.directive.spec.ts +++ b/projects/ion/src/lib/tooltip/tooltip.directive.spec.ts @@ -100,17 +100,6 @@ describe('Directive: Tooltip', () => { } ); - it.each(Object.values(TooltipPosition))( - 'should render tooltip on %s position', - async (ionTooltipPosition) => { - await sut({ ionTooltipPosition }); - fireEvent.mouseEnter(screen.getByTestId('hostTooltip')); - expect(screen.getByTestId('ion-tooltip')).toHaveClass( - `ion-tooltip-position--${ionTooltipPosition}` - ); - } - ); - it('should show tooltip after delay time setted', async () => { jest.useFakeTimers(); const timeDelay = 300; @@ -133,6 +122,14 @@ describe('Directive: Tooltip', () => { ); }); + it('should reposition the tooltip when exceed the screen size', async () => { + await sut(); + fireEvent.mouseEnter(screen.getByTestId('hostTooltip')); + expect(screen.getByTestId('ion-tooltip')).toHaveClass( + `ion-tooltip-position--bottomRight` + ); + }); + describe('trigger: click', () => { afterEach(async () => { fireEvent.click(screen.getByTestId('hostTooltip')); diff --git a/projects/ion/src/lib/tooltip/tooltip.directive.ts b/projects/ion/src/lib/tooltip/tooltip.directive.ts index 78e445a79..8fcafee40 100644 --- a/projects/ion/src/lib/tooltip/tooltip.directive.ts +++ b/projects/ion/src/lib/tooltip/tooltip.directive.ts @@ -1,3 +1,4 @@ +import { TooltipPosition } from './../core/types/tooltip'; import { ApplicationRef, ComponentFactoryResolver, @@ -9,21 +10,20 @@ import { Injector, Input, OnDestroy, + OnInit, TemplateRef, } from '@angular/core'; -import { - TooltipColorScheme, - TooltipPosition, - TooltipTrigger, -} from '../core/types'; +import { TooltipColorScheme, TooltipTrigger } from '../core/types'; import { SafeAny } from '../utils/safe-any'; import { IonTooltipComponent } from './tooltip.component'; import { getPositions } from './utilsTooltip'; +import { TooltipService } from './tooltip.service'; +import { Subscription } from 'rxjs'; @Directive({ selector: '[ionTooltip]', }) -export class IonTooltipDirective implements OnDestroy { +export class IonTooltipDirective implements OnDestroy, OnInit { @Input() ionTooltipTitle = ''; @Input() ionTooltipTemplateRef: TemplateRef; @Input() ionTooltipColorScheme: TooltipColorScheme = 'dark'; @@ -32,6 +32,7 @@ export class IonTooltipDirective implements OnDestroy { @Input() ionTooltipTrigger: TooltipTrigger = TooltipTrigger.DEFAULT; @Input() ionTooltipShowDelay = 0; + public subscription$: Subscription; private componentRef: ComponentRef = null; private delayTimeout: number; @@ -39,9 +40,18 @@ export class IonTooltipDirective implements OnDestroy { private componentFactoryResolver: ComponentFactoryResolver, private appRef: ApplicationRef, private injector: Injector, - private elementRef: ElementRef + private elementRef: ElementRef, + private tooltipService: TooltipService ) {} + ngOnInit(): void { + this.subscription$ = this.tooltipService.reposition.subscribe(() => { + if (!this.isComponentRefNull()) { + this.setComponentPosition(); + } + }); + } + isComponentRefNull(): boolean { return this.componentRef === null; } @@ -77,7 +87,6 @@ export class IonTooltipDirective implements OnDestroy { this.showTooltip.bind(this), this.ionTooltipShowDelay ); - this.setComponentPosition(); } } @@ -87,13 +96,18 @@ export class IonTooltipDirective implements OnDestroy { this.elementRef.nativeElement.getBoundingClientRect(); const hostPositions = { left, right, top, bottom }; + + this.tooltipService.setHostPosition(hostPositions); + const positions = getPositions( hostPositions, this.ionTooltipArrowPointAtCenter ); - this.componentRef.instance.left = positions[this.ionTooltipPosition].left; - this.componentRef.instance.top = positions[this.ionTooltipPosition].top; + this.componentRef.instance.left = + positions[this.componentRef.instance.ionTooltipPosition].left; + this.componentRef.instance.top = + positions[this.componentRef.instance.ionTooltipPosition].top; } attachComponentToView(): void { @@ -145,5 +159,6 @@ export class IonTooltipDirective implements OnDestroy { ngOnDestroy(): void { this.destroyComponent(); + this.subscription$.unsubscribe(); } } diff --git a/projects/ion/src/lib/tooltip/tooltip.module.ts b/projects/ion/src/lib/tooltip/tooltip.module.ts index 6f3d871c6..73683e0ce 100644 --- a/projects/ion/src/lib/tooltip/tooltip.module.ts +++ b/projects/ion/src/lib/tooltip/tooltip.module.ts @@ -2,11 +2,13 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { IonTooltipComponent } from './tooltip.component'; import { IonTooltipDirective } from './tooltip.directive'; +import { TooltipService } from './tooltip.service'; @NgModule({ declarations: [IonTooltipComponent, IonTooltipDirective], imports: [CommonModule], exports: [IonTooltipDirective], entryComponents: [IonTooltipComponent], + providers: [TooltipService], }) export class IonTooltipModule {} diff --git a/projects/ion/src/lib/tooltip/tooltip.service.spec.ts b/projects/ion/src/lib/tooltip/tooltip.service.spec.ts new file mode 100644 index 000000000..58ebc4c28 --- /dev/null +++ b/projects/ion/src/lib/tooltip/tooltip.service.spec.ts @@ -0,0 +1,95 @@ +import { TestBed, getTestBed } from '@angular/core/testing'; +import { TooltipService } from './tooltip.service'; + +const screenWidth = 1440; +const screenHeight = 900; +const tooltipPositions = { + centerRight: { + tooltipCoordinates: { + left: 1200, + height: 88, + width: 300, + }, + tooltipHeight: 88, + hostPosition: { top: 450, bottom: 538, left: 1200, right: 1380 }, + screenWidth, + screenHeight, + }, + bottomCenter: { + tooltipCoordinates: { + left: 700, + height: 88, + width: 300, + }, + hostPosition: { top: 450, bottom: 820, left: 700, right: 880 }, + screenWidth, + screenHeight, + }, + centerLeft: { + tooltipCoordinates: { + left: -20, + height: 88, + width: 300, + }, + hostPosition: { top: 750, bottom: 650, left: 0, right: 80 }, + screenWidth, + screenHeight, + }, + topRight: { + tooltipCoordinates: { + left: 1380, + height: 88, + width: 300, + }, + hostPosition: { top: 0, bottom: 100, left: 1440, right: 1540 }, + screenWidth, + screenHeight, + }, + bottomLeft: { + tooltipCoordinates: { + left: -20, + height: 88, + width: 300, + }, + hostPosition: { top: 880, bottom: 900, left: 0, right: 80 }, + screenWidth, + screenHeight, + }, + bottomRight: { + tooltipCoordinates: { + left: 1300, + height: 88, + width: 300, + }, + hostPosition: { top: 880, bottom: 900, left: 1380, right: 1440 }, + screenWidth, + screenHeight, + }, +}; + +let injector: TestBed; +let service: TooltipService; + +const sut = async (): Promise => { + TestBed.configureTestingModule({ + providers: [TooltipService], + }); + + injector = getTestBed(); + service = injector.get(TooltipService); +}; + +describe('TooltipService', () => { + beforeEach(async () => { + await sut(); + }); + + it.each(Object.entries(tooltipPositions))( + 'should return %s as the new position if at the edges', + async (positionKey, positionValue) => { + const positionChecks = service.getTooltipPositions(positionValue); + const newPosition = service.checkPositions(positionChecks); + expect(newPosition).toBe(positionKey); + } + ); +}); diff --git a/projects/ion/src/lib/tooltip/tooltip.service.ts b/projects/ion/src/lib/tooltip/tooltip.service.ts new file mode 100644 index 000000000..ecf59e8bd --- /dev/null +++ b/projects/ion/src/lib/tooltip/tooltip.service.ts @@ -0,0 +1,127 @@ +import { Injectable } from '@angular/core'; +import { Subject } from 'rxjs'; +import { TooltipPosition } from '../core/types'; + +export type RepositionData = { + tooltipCoordinates: Partial; + hostPosition: Partial; + screenWidth: number; + screenHeight: number; +}; + +export type tooltipPositionChecks = { + [x in TooltipPosition]?: boolean; +}; + +@Injectable({ + providedIn: 'root', +}) +export class TooltipService { + public readonly reposition = new Subject(); + private hostPosition: Partial; + private tootipCoordinates: DOMRect; + private elementPadding = 16; + private currentPosition: TooltipPosition; + + public setHostPosition(position: Partial): void { + this.hostPosition = position; + } + + public setTooltipCoordinates(coordinates: DOMRect): void { + this.tootipCoordinates = coordinates; + } + + public setCurrentPosition(position: TooltipPosition): void { + this.currentPosition = position; + } + + public getNewPosition(): TooltipPosition { + const { clientWidth, clientHeight } = document.body; + + if (!this.hostPosition) { + return; + } + + const repositionData: RepositionData = { + tooltipCoordinates: this.tootipCoordinates, + hostPosition: this.hostPosition, + screenWidth: clientWidth, + screenHeight: clientHeight, + }; + const positions: tooltipPositionChecks = + this.getTooltipPositions(repositionData); + + return this.checkPositions(positions); + } + + public checkPositions(positions: tooltipPositionChecks): TooltipPosition { + let newPosition = this.currentPosition; + + Object.entries(positions).forEach(([position, check]) => { + if (check) { + newPosition = position as TooltipPosition; + } + }); + + return newPosition; + } + + public getTooltipPositions( + respositionData: RepositionData + ): tooltipPositionChecks { + const { height, left, width } = respositionData.tooltipCoordinates; + + const tooltipPositions = { + right: this.atRightEdge( + respositionData.screenWidth, + respositionData.hostPosition.right, + width + ), + bottom: this.atBottomEdge( + respositionData.screenHeight, + respositionData.hostPosition.bottom, + height + ), + left: this.atLeftEdge(left, width), + top: this.atTopEdge(respositionData.hostPosition.top, height), + }; + + return { + centerRight: tooltipPositions.right, + bottomCenter: tooltipPositions.bottom, + centerLeft: tooltipPositions.left, + topRight: tooltipPositions.top && tooltipPositions.right, + bottomLeft: tooltipPositions.bottom && tooltipPositions.left, + topLeft: tooltipPositions.top && tooltipPositions.left, + bottomRight: tooltipPositions.bottom && tooltipPositions.right, + }; + } + + public emitReposition(): void { + this.reposition.next(); + } + + private atRightEdge( + screenWidth: number, + hostRight: number, + tooltipWidth: number + ): boolean { + return screenWidth - hostRight < (tooltipWidth + this.elementPadding) / 2; + } + + private atBottomEdge( + screenHeight: number, + hostBottom: number, + tooltipHeight: number + ): boolean { + return screenHeight - hostBottom < tooltipHeight + this.elementPadding; + } + + private atLeftEdge(tooltipLeft: number, tooltipWidth: number): boolean { + return tooltipLeft < (tooltipWidth + this.elementPadding) / 2; + } + + private atTopEdge(hostTop: number, tooltipHeight: number): boolean { + return hostTop < (tooltipHeight + this.elementPadding) / 2; + } +} diff --git a/projects/ion/src/lib/use-table/use-table.component.ts b/projects/ion/src/lib/use-table/use-table.component.ts index 27c095eda..5583cda51 100644 --- a/projects/ion/src/lib/use-table/use-table.component.ts +++ b/projects/ion/src/lib/use-table/use-table.component.ts @@ -8,6 +8,39 @@ interface User { data_criacao: string; } +const columns = [ + { + key: 'data_monitoramento', + label: 'Data do monitoramento', + sort: true, + }, + { + key: 'http_code', + label: 'HTTP code', + sort: true, + }, + { + key: 'dns', + label: 'DNS', + sort: true, + }, + { + key: 'latencia', + label: 'Conexão', + sort: true, + }, + { + key: 'processamento', + label: 'Processamento', + sort: true, + }, + { + key: 'problemas', + label: 'Problemas', + sort: true, + }, +]; + @Component({ selector: 'ion-use-table', templateUrl: './use-table.component.html', @@ -17,10 +50,7 @@ export class IonUseTableComponent extends BnTable { super({ service, tableConfig: { - columns: [ - { label: 'Nome', sort: true, key: 'nome', desc: true }, - { label: 'Url', sort: true, key: 'url' }, - ], + columns, actions: [ { icon: 'trash', diff --git a/stories/Tooltip.stories.ts b/stories/Tooltip.stories.ts index a6ccbcc64..84cf9b1b6 100644 --- a/stories/Tooltip.stories.ts +++ b/stories/Tooltip.stories.ts @@ -111,6 +111,7 @@ const WithTemplateRef: Story = (args) => ({
({ export const WithContent = WithTemplateRef.bind({}); WithContent.args = { ionTooltipTitle: '', - ionTooltipPosition: TooltipPosition.DEFAULT, + ionTooltipPosition: TooltipPosition.BOTTOM_CENTER, ionTooltipTrigger: TooltipTrigger.DEFAULT, } as TooltipProps; WithContent.storyName = 'With Content'; @@ -189,3 +190,140 @@ WithTitleAndSubtitle.args = { ionTooltipTrigger: TooltipTrigger.DEFAULT, } as TooltipProps; WithTitleAndSubtitle.storyName = 'With Title and Content'; + +const TemplateWithEdgeHost: Story = (args) => ({ + props: args, + template: ` + +
+
+ + Hover me + + + Hover me + + + Hover me + +
+
+ + Hover me + + + Hover me + + + Hover me + +
+
+ + Hover me + + + Hover me + + + Hover me + +
+
+ `, +}); + +export const WithHostOnEdge = TemplateWithEdgeHost.bind({}); +WithHostOnEdge.args = { + ionTooltipTitle: + 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.', + ionTooltipPosition: TooltipPosition.DEFAULT, + ionTooltipTrigger: TooltipTrigger.DEFAULT, +} as TooltipProps;