-
Notifications
You must be signed in to change notification settings - Fork 32
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
Use a proper tooltip for the Metadata Quality widget #952
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
<div class="ml-4 flex flex-row"> | ||
<mat-icon class="material-symbols-outlined">{{ icon }}</mat-icon> | ||
<mat-icon class="material-symbols-outlined min-w-fit">{{ icon }}</mat-icon> | ||
<p class="ml-2 text">{{ labelKey | translate }}</p> | ||
</div> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,21 @@ | ||
<div | ||
*ngIf="metadataQualityDisplay" | ||
class="mb-6 metadata-quality" | ||
(mouseenter)="showMenu()" | ||
(mouseleave)="hideMenu()" | ||
> | ||
<div class="min-w-[200px]" [class]="smaller ? 'leading-[8px]' : ''"> | ||
<gn-ui-progress-bar | ||
(focus)="showMenu()" | ||
(blur)="hideMenu()" | ||
tabindex="0" | ||
[value]="qualityScore" | ||
type="primary" | ||
></gn-ui-progress-bar> | ||
</div> | ||
<div | ||
class="absolute z-10 bg-white border border-black border-opacity-35 rounded-lg shadow-lg p-5 whitespace-nowrap" | ||
[class]="isMenuShown ? 'block' : 'hidden'" | ||
> | ||
<div *ngIf="metadataQualityDisplay" class="mb-6 metadata-quality"> | ||
<gn-ui-popover [content]="popoverItems" theme="light-border"> | ||
<div class="min-w-[200px]" [class]="smaller ? 'leading-[8px]' : ''"> | ||
<gn-ui-progress-bar | ||
tabindex="0" | ||
[value]="qualityScore" | ||
type="primary" | ||
></gn-ui-progress-bar> | ||
</div> | ||
</gn-ui-popover> | ||
</div> | ||
<ng-template #popoverItems> | ||
<div class="p-2 py-4"> | ||
<div class="mb-4 font-bold" translate>record.metadata.quality.details</div> | ||
<gn-ui-metadata-quality-item | ||
*ngFor="let e of items" | ||
[name]="e.name" | ||
[value]="e.value" | ||
></gn-ui-metadata-quality-item> | ||
</div> | ||
</div> | ||
</ng-template> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,7 +9,10 @@ import { | |
} from '@geonetwork-ui/util/i18n' | ||
import { TranslateModule } from '@ngx-translate/core' | ||
import { MetadataQualityItemComponent } from '../metadata-quality-item/metadata-quality-item.component' | ||
import { ProgressBarComponent } from '@geonetwork-ui/ui/widgets' | ||
import { | ||
PopoverComponent, | ||
ProgressBarComponent, | ||
} from '@geonetwork-ui/ui/widgets' | ||
import { UtilSharedModule } from '@geonetwork-ui/util/shared' | ||
import { By } from '@angular/platform-browser' | ||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core' | ||
|
@@ -44,6 +47,7 @@ describe('MetadataQualityComponent', () => { | |
MatIconModule, | ||
UtilI18nModule, | ||
TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), | ||
PopoverComponent, | ||
], | ||
}).compileComponents() | ||
}) | ||
|
@@ -61,28 +65,6 @@ describe('MetadataQualityComponent', () => { | |
expect(component).toBeTruthy() | ||
}) | ||
|
||
it('focus should show menu / blur should hide', () => { | ||
const progressBar = fixture.debugElement.query(By.css('gn-ui-progress-bar')) | ||
progressBar.nativeElement.focus() | ||
expect(component.isMenuShown).toBe(true) | ||
progressBar.nativeElement.blur() | ||
expect(component.isMenuShown).toBe(false) | ||
}) | ||
|
||
it('mouseenter should show menu / mouseleave should hide', () => { | ||
const metadataQuality = fixture.debugElement.query( | ||
By.css('.metadata-quality') | ||
) | ||
|
||
const mouseEnterEvent = new Event('mouseenter') | ||
metadataQuality.nativeElement.dispatchEvent(mouseEnterEvent) | ||
expect(component.isMenuShown).toBe(true) | ||
|
||
const mouseLeaveEvent = new Event('mouseleave') | ||
metadataQuality.nativeElement.dispatchEvent(mouseLeaveEvent) | ||
expect(component.isMenuShown).toBe(false) | ||
}) | ||
|
||
it('content', () => { | ||
expect(component.metadata?.contacts[0]?.email).toBe('[email protected]') | ||
}) | ||
|
@@ -94,6 +76,11 @@ describe('MetadataQualityComponent', () => { | |
}) | ||
|
||
it('should display sub-components with correct inputs', () => { | ||
const popoverElement = fixture.debugElement.query( | ||
By.directive(PopoverComponent) | ||
) | ||
popoverElement.triggerEventHandler('mouseenter', null) | ||
fixture.detectChanges() | ||
const metadataItems = fixture.debugElement.queryAll( | ||
By.directive(MetadataQualityItemComponent) | ||
) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
<span #popoverContent> | ||
<ng-content></ng-content> | ||
</span> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { ComponentFixture, TestBed } from '@angular/core/testing' | ||
import { PopoverComponent } from './popover.component' | ||
import { ElementRef } from '@angular/core' | ||
|
||
describe('PopoverComponent', () => { | ||
let component: PopoverComponent | ||
let fixture: ComponentFixture<PopoverComponent> | ||
|
||
beforeEach(async () => { | ||
await TestBed.configureTestingModule({ | ||
imports: [PopoverComponent], | ||
}).compileComponents() | ||
}) | ||
|
||
beforeEach(() => { | ||
fixture = TestBed.createComponent(PopoverComponent) | ||
component = fixture.componentInstance | ||
component.content = 'Test tooltip content' | ||
fixture.detectChanges() | ||
}) | ||
|
||
it('should create', () => { | ||
expect(component).toBeTruthy() | ||
}) | ||
|
||
it('should initialize tippy instance on view init', () => { | ||
const elementRef = new ElementRef(document.createElement('div')) | ||
component.popoverContent = elementRef | ||
component.ngAfterViewInit() | ||
expect(component['tippyInstance']).toBeDefined() | ||
}) | ||
|
||
it('should destroy tippy instance on destroy', () => { | ||
const elementRef = new ElementRef(document.createElement('div')) | ||
component.popoverContent = elementRef | ||
component.ngAfterViewInit() | ||
let destroyCalled = false | ||
component['tippyInstance'].destroy = () => { | ||
destroyCalled = true | ||
} | ||
component.ngOnDestroy() | ||
expect(destroyCalled).toBe(true) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { Meta, Story } from '@storybook/angular' | ||
import { PopoverComponent } from './popover.component' | ||
import { moduleMetadata } from '@storybook/angular' | ||
import { CommonModule } from '@angular/common' | ||
|
||
export default { | ||
title: 'Widgets/Popover', | ||
component: PopoverComponent, | ||
decorators: [ | ||
moduleMetadata({ | ||
imports: [CommonModule], | ||
}), | ||
], | ||
argTypes: { | ||
content: { control: 'text' }, | ||
theme: { | ||
control: 'select', | ||
options: ['', 'light', 'light-border', 'translucent', 'material'], | ||
}, | ||
}, | ||
} as Meta | ||
|
||
const Template: Story<PopoverComponent> = (args: PopoverComponent) => ({ | ||
component: PopoverComponent, | ||
props: args, | ||
template: `<gn-ui-popover [content]="content" [theme]="theme">Hover me to see tooltip</gn-ui-popover>`, | ||
}) | ||
|
||
export const Default = Template.bind({}) | ||
Default.args = { | ||
content: 'Default tooltip content', | ||
theme: '', | ||
} | ||
|
||
export const TemplateContent: Story<PopoverComponent> = ( | ||
args: PopoverComponent | ||
) => ({ | ||
component: PopoverComponent, | ||
template: ` | ||
<ng-template #popoverTemplate> | ||
<div> | ||
<strong>Tooltip Header</strong> | ||
<p>Detailed information about the tooltip.</p> | ||
</div> | ||
</ng-template> | ||
<gn-ui-popover [content]="popoverTemplate" [theme]="theme"> | ||
Hover me to see tooltip | ||
</gn-ui-popover> | ||
`, | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { CommonModule } from '@angular/common' | ||
import { | ||
Component, | ||
AfterViewInit, | ||
ElementRef, | ||
Input, | ||
ViewChild, | ||
OnDestroy, | ||
OnChanges, | ||
SimpleChanges, | ||
TemplateRef, | ||
Renderer2, | ||
ViewContainerRef, | ||
EmbeddedViewRef, | ||
} from '@angular/core' | ||
import tippy, { Instance } from 'tippy.js' | ||
|
||
@Component({ | ||
selector: 'gn-ui-popover', | ||
templateUrl: './popover.component.html', | ||
styleUrls: ['./popover.component.css'], | ||
standalone: true, | ||
imports: [CommonModule], | ||
}) | ||
export class PopoverComponent implements AfterViewInit, OnChanges, OnDestroy { | ||
@ViewChild('popoverContent', { static: false }) popoverContent: ElementRef | ||
@Input() content: string | TemplateRef<any> | ||
@Input() theme: 'light' | 'light-border' | 'translucent' | 'material' | '' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Somehow the theming doesn't seem to get applied. Not in the storybook nor in the datahub. Is there something I'm missing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. my screenshoot come from the story-book using light-border. Your screenshoot looks like the default theming same as ''. I don't know yet why it doesn't apply on your side. Which browser do you use? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now the theming works on my side as well. Not sure what was the issue. Never mind! Looks good to me. |
||
|
||
private tippyInstance: Instance | ||
private view: EmbeddedViewRef<any> | ||
|
||
constructor( | ||
private viewContainerRef: ViewContainerRef, | ||
private renderer: Renderer2 | ||
) {} | ||
|
||
private getContent(): string | HTMLElement { | ||
if (this.content instanceof TemplateRef) { | ||
if (this.view) { | ||
this.view.destroy() | ||
} | ||
this.view = this.viewContainerRef.createEmbeddedView(this.content) | ||
this.view.detectChanges() | ||
const wrapper = this.renderer.createElement('div') // Create a wrapper div | ||
this.view.rootNodes.forEach((node) => { | ||
this.renderer.appendChild(wrapper, node) // Append each root node to the wrapper | ||
}) | ||
return wrapper | ||
} | ||
return this.content | ||
} | ||
|
||
ngAfterViewInit(): void { | ||
this.tippyInstance = tippy(this.popoverContent.nativeElement as Element, { | ||
content: this.getContent(), | ||
allowHTML: true, | ||
theme: this.theme, | ||
}) | ||
} | ||
|
||
ngOnChanges(changes: SimpleChanges): void { | ||
if (changes['theme']) { | ||
this.theme = changes['theme'].currentValue | ||
if (this.tippyInstance) { | ||
this.tippyInstance.setProps({ theme: this.theme }) | ||
} | ||
} | ||
if (changes['content']) { | ||
this.content = changes['content'].currentValue | ||
if (this.tippyInstance) { | ||
this.tippyInstance.setContent(this.getContent()) | ||
} | ||
} | ||
} | ||
|
||
ngOnDestroy(): void { | ||
if (this.tippyInstance) { | ||
this.tippyInstance.destroy() | ||
} | ||
if (this.view) { | ||
this.view.destroy() | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please make this component standalone? this is what we do for every new component now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is standalone component, and what does it apply ? i had based my code on ProgressBarComponent which is not standalone
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://blog.angular-university.io/angular-standalone-components/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jahow I have done it yersterday, and i fixed the lint error this morning should be ok now