diff --git a/apps/demo/project.json b/apps/demo/project.json
index d6a3bf225f..b22e02d35c 100644
--- a/apps/demo/project.json
+++ b/apps/demo/project.json
@@ -25,6 +25,11 @@
}
],
"styles": [
+ "node_modules/tippy.js/dist/tippy.css",
+ "node_modules/tippy.js/themes/light.css",
+ "node_modules/tippy.js/themes/light-border.css",
+ "node_modules/tippy.js/themes/material.css",
+ "node_modules/tippy.js/themes/translucent.css",
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
"apps/demo/src/styles.css",
"tailwind.base.css"
diff --git a/libs/ui/elements/src/lib/metadata-quality-item/metadata-quality-item.component.html b/libs/ui/elements/src/lib/metadata-quality-item/metadata-quality-item.component.html
index 09036c9b6c..397f7b8eda 100644
--- a/libs/ui/elements/src/lib/metadata-quality-item/metadata-quality-item.component.html
+++ b/libs/ui/elements/src/lib/metadata-quality-item/metadata-quality-item.component.html
@@ -1,4 +1,4 @@
-
{{ icon }}
+
{{ icon }}
{{ labelKey | translate }}
diff --git a/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.html b/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.html
index d56a5c1067..392a4d748c 100644
--- a/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.html
+++ b/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.html
@@ -1,22 +1,16 @@
-
-
-
-
-
+
+
+
record.metadata.quality.details
-
+
diff --git a/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.spec.ts b/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.spec.ts
index 0624689541..2b226cfd04 100644
--- a/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.spec.ts
+++ b/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.spec.ts
@@ -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('bob@org.net')
})
@@ -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)
)
diff --git a/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.stories.ts b/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.stories.ts
index cf67b7a402..6b22590dcb 100644
--- a/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.stories.ts
+++ b/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.stories.ts
@@ -9,6 +9,7 @@ import {
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 } from '@geonetwork-ui/ui/widgets'
import { MatIconModule } from '@angular/material/icon'
export default {
@@ -22,6 +23,7 @@ export default {
MatIconModule,
UtilI18nModule,
TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG),
+ PopoverComponent,
],
}),
],
diff --git a/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.ts b/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.ts
index e6d86036e1..6f604eb608 100644
--- a/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.ts
+++ b/libs/ui/elements/src/lib/metadata-quality/metadata-quality.component.ts
@@ -21,8 +21,6 @@ export class MetadataQualityComponent implements OnChanges {
items: MetadataQualityItem[] = []
- isMenuShown = false
-
get qualityScore() {
const qualityScore = this.metadata?.extras?.qualityScore
return typeof qualityScore === 'number'
@@ -36,14 +34,6 @@ export class MetadataQualityComponent implements OnChanges {
)
}
- showMenu() {
- this.isMenuShown = true
- }
-
- hideMenu() {
- this.isMenuShown = false
- }
-
private add(name: string, value: boolean) {
if (this.metadataQualityDisplay?.[name] !== false) {
this.items.push({ name, value })
diff --git a/libs/ui/elements/src/lib/ui-elements.module.ts b/libs/ui/elements/src/lib/ui-elements.module.ts
index 274cbc06fe..d99869b046 100644
--- a/libs/ui/elements/src/lib/ui-elements.module.ts
+++ b/libs/ui/elements/src/lib/ui-elements.module.ts
@@ -9,7 +9,7 @@ import { ContentGhostComponent } from './content-ghost/content-ghost.component'
import { DownloadItemComponent } from './download-item/download-item.component'
import { DownloadsListComponent } from './downloads-list/downloads-list.component'
import { ApiCardComponent } from './api-card/api-card.component'
-import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets'
+import { PopoverComponent, UiWidgetsModule } from '@geonetwork-ui/ui/widgets'
import { MaxLinesComponent, UiLayoutModule } from '@geonetwork-ui/ui/layout'
import { TranslateModule } from '@ngx-translate/core'
import { RelatedRecordCardComponent } from './related-record-card/related-record-card.component'
@@ -45,6 +45,7 @@ import { TimeSincePipe } from './user-feedback-item/time-since.pipe'
UiInputsModule,
FormsModule,
NgOptimizedImage,
+ PopoverComponent,
MarkdownParserComponent,
ThumbnailComponent,
TimeSincePipe,
diff --git a/libs/ui/widgets/src/index.ts b/libs/ui/widgets/src/index.ts
index 26b0aef91e..3100b6e6e8 100644
--- a/libs/ui/widgets/src/index.ts
+++ b/libs/ui/widgets/src/index.ts
@@ -1,5 +1,6 @@
export * from './lib/ui-widgets.module'
export * from './lib/progress-bar/progress-bar.component'
+export * from './lib/popover/popover.component'
export * from './lib/loading-mask/loading-mask.component'
export * from './lib/color-scale/color-scale.component'
export * from './lib/popup-alert/popup-alert.component'
diff --git a/libs/ui/widgets/src/lib/popover/popover.component.css b/libs/ui/widgets/src/lib/popover/popover.component.css
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/libs/ui/widgets/src/lib/popover/popover.component.html b/libs/ui/widgets/src/lib/popover/popover.component.html
new file mode 100644
index 0000000000..08a008c9e8
--- /dev/null
+++ b/libs/ui/widgets/src/lib/popover/popover.component.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/libs/ui/widgets/src/lib/popover/popover.component.spec.ts b/libs/ui/widgets/src/lib/popover/popover.component.spec.ts
new file mode 100644
index 0000000000..5ad2ac6433
--- /dev/null
+++ b/libs/ui/widgets/src/lib/popover/popover.component.spec.ts
@@ -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
+
+ 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)
+ })
+})
diff --git a/libs/ui/widgets/src/lib/popover/popover.component.stories.ts b/libs/ui/widgets/src/lib/popover/popover.component.stories.ts
new file mode 100644
index 0000000000..e56a2ec554
--- /dev/null
+++ b/libs/ui/widgets/src/lib/popover/popover.component.stories.ts
@@ -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 = (args: PopoverComponent) => ({
+ component: PopoverComponent,
+ props: args,
+ template: `Hover me to see tooltip`,
+})
+
+export const Default = Template.bind({})
+Default.args = {
+ content: 'Default tooltip content',
+ theme: '',
+}
+
+export const TemplateContent: Story = (
+ args: PopoverComponent
+) => ({
+ component: PopoverComponent,
+ template: `
+
+
+
Tooltip Header
+
Detailed information about the tooltip.
+
+
+
+ Hover me to see tooltip
+
+ `,
+})
diff --git a/libs/ui/widgets/src/lib/popover/popover.component.ts b/libs/ui/widgets/src/lib/popover/popover.component.ts
new file mode 100644
index 0000000000..a167101c03
--- /dev/null
+++ b/libs/ui/widgets/src/lib/popover/popover.component.ts
@@ -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
+ @Input() theme: 'light' | 'light-border' | 'translucent' | 'material' | ''
+
+ private tippyInstance: Instance
+ private view: EmbeddedViewRef
+
+ 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()
+ }
+ }
+}