diff --git a/apps/webcomponents/src/app/AppOverlayContainer.ts b/apps/webcomponents/src/app/AppOverlayContainer.ts deleted file mode 100644 index 73c6f3249f..0000000000 --- a/apps/webcomponents/src/app/AppOverlayContainer.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { OverlayContainer } from '@angular/cdk/overlay' -import { Platform } from '@angular/cdk/platform' -import { DOCUMENT } from '@angular/common' -import { Inject, Injectable, OnDestroy } from '@angular/core' - -@Injectable() -export class AppOverlayContainer extends OverlayContainer implements OnDestroy { - private selector: string - - constructor( - @Inject(DOCUMENT) private document: Document, - platform: Platform - ) { - super(document, platform) - } - - setSelector(selector: string) { - this.selector = selector - } - ngOnDestroy() { - super.ngOnDestroy() - } - - protected _createContainer(): void { - const container: HTMLDivElement = this.document.createElement('div') - container.classList.add('app-overlay-container') - const element: Element | null = this.document - .querySelector(this.selector) - .shadowRoot.querySelector('#angular-app-root') - if (element !== null) { - element.appendChild(container) - this._containerElement = container - } else { - console.error( - 'Material CDK Overlay creation failed ! ' + - 'It can work only with gn-search-input webcomponent. ' + - 'You have to add an element with id="angular-app-root" (in the shadowDOM) to which the overlay will be appended.' - ) - } - } -} diff --git a/apps/webcomponents/src/app/components/base.component.ts b/apps/webcomponents/src/app/components/base.component.ts index 0c3a3b63e7..59394945c8 100644 --- a/apps/webcomponents/src/app/components/base.component.ts +++ b/apps/webcomponents/src/app/components/base.component.ts @@ -1,4 +1,11 @@ -import { Component, Injector, Input, OnChanges, OnInit } from '@angular/core' +import { + Component, + ElementRef, + Injector, + Input, + OnChanges, + OnInit, +} from '@angular/core' import { LinkClassifierService, LinkUsage, @@ -10,6 +17,8 @@ import { TranslateService } from '@ngx-translate/core' import { firstValueFrom } from 'rxjs' import { DatasetDistribution } from '@geonetwork-ui/common/domain/record' import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/records-repository.interface' +import { OverlayContainer } from '@angular/cdk/overlay' +import { WebcomponentOverlayContainer } from '../webcomponent-overlay-container' export const apiConfiguration = new Configuration() @@ -40,6 +49,12 @@ export class BaseComponent implements OnChanges, OnInit { this.searchService = injector.get(SearchApiService) this.recordsRepository = injector.get(RecordsRepositoryInterface) this.linkClassifier = injector.get(LinkClassifierService) + + const elementRef = injector.get(ElementRef) + const overlayContainer = injector.get( + OverlayContainer + ) as WebcomponentOverlayContainer + overlayContainer.setRoot(elementRef.nativeElement.shadowRoot) } ngOnInit() { @@ -68,6 +83,7 @@ export class BaseComponent implements OnChanges, OnInit { this.titleFont ) this.facade.init(this.searchId) + this.copyFontFacesToDocument() this.isInitialized = true } @@ -75,6 +91,27 @@ export class BaseComponent implements OnChanges, OnInit { // to override } + private copyFontFacesToDocument() { + // get the list of font face definitions in the Shadow DOM + const root = this.injector.get(ElementRef).nativeElement as HTMLElement + const styles = root.shadowRoot.styleSheets + const fontFaces = Array.from(styles).reduce( + (prev, curr) => [ + ...prev, + ...Array.from(curr.cssRules) + .filter((rule) => rule.cssText.startsWith('@font-face')) + .map((rule) => rule.cssText), + ], + [] + ) + + // all font faces are then copied to the document + const style = document.createElement('style') + const cssText = fontFaces.join('\n') + style.appendChild(document.createTextNode(cssText)) + document.head.appendChild(style) + } + async getRecordLink( uuid: string, usages: LinkUsage[] diff --git a/apps/webcomponents/src/app/components/gn-dataset-view-chart/gn-dataset-view-chart.sample.html b/apps/webcomponents/src/app/components/gn-dataset-view-chart/gn-dataset-view-chart.sample.html index d875ef9b79..aa88dcf175 100644 --- a/apps/webcomponents/src/app/components/gn-dataset-view-chart/gn-dataset-view-chart.sample.html +++ b/apps/webcomponents/src/app/components/gn-dataset-view-chart/gn-dataset-view-chart.sample.html @@ -7,10 +7,6 @@ - - - - - - - - - + diff --git a/apps/webcomponents/src/app/components/gn-search-input/gn-search-input.sample.html b/apps/webcomponents/src/app/components/gn-search-input/gn-search-input.sample.html index a9669e8fa7..2ac92548fa 100644 --- a/apps/webcomponents/src/app/components/gn-search-input/gn-search-input.sample.html +++ b/apps/webcomponents/src/app/components/gn-search-input/gn-search-input.sample.html @@ -7,10 +7,6 @@ - BaseComponent, string][] = [ }, { provide: OverlayContainer, - useFactory: (document: Document, platform: Platform) => { - const container = new AppOverlayContainer(document, platform) - container.setSelector('gn-search-input') - return container - }, - deps: [DOCUMENT, Platform], + useClass: WebcomponentOverlayContainer, }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], diff --git a/apps/webcomponents/src/styles.css b/apps/webcomponents/src/styles.css index 4814c8c96c..799b8b6c3e 100644 --- a/apps/webcomponents/src/styles.css +++ b/apps/webcomponents/src/styles.css @@ -13,7 +13,7 @@ font-family: 'Material Symbols Outlined'; font-style: normal; font-weight: 400; - src: url(https://fonts.gstatic.com/s/materialiconsoutlined/v108/gok-H7zzDkdnRel8-DQ6KAXJ69wP1tGnf4ZGhUce.woff2) + src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v138/kJEhBvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oFsLjBuVY.woff2) format('woff2'); } .material-symbols-outlined { @@ -32,7 +32,7 @@ -webkit-font-smoothing: antialiased; } -/* Material Theme */ +/* These classes were extracted from the full Material theme to save size */ .cdk-overlay-pane { position: absolute; pointer-events: auto; @@ -42,7 +42,6 @@ max-width: 100%; max-height: 100%; } - .cdk-overlay-connected-position-bounding-box { position: absolute; z-index: 1000; @@ -51,7 +50,31 @@ min-width: 1px; min-height: 1px; } -.app-overlay-container { +.cdk-overlay-backdrop { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 1000; + pointer-events: auto; + transition: opacity 400ms cubic-bezier(0.25, 0.8, 0.25, 1); + opacity: 0; +} +.cdk-overlay-backdrop.cdk-overlay-backdrop-showing { + opacity: 1; +} +.cdk-overlay-transparent-backdrop { + transition: visibility 1ms linear, opacity 1ms linear; + visibility: hidden; + opacity: 1; +} +.cdk-overlay-transparent-backdrop.cdk-overlay-backdrop-showing { + opacity: 0; + visibility: visible; +} + +.gn-ui-overlay-container { position: absolute; z-index: 1000; pointer-events: none; @@ -60,7 +83,7 @@ height: 100%; width: 100%; } -.app-overlay-container:empty { +.gn-ui-overlay-container:empty { display: none; } diff --git a/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.html b/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.html index ea7fcafe9e..0ce3113a7f 100644 --- a/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.html +++ b/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.html @@ -4,6 +4,7 @@ [action]="autoCompleteAction" (itemSelected)="handleItemSelection($event)" (inputSubmitted)="handleInputSubmission($event)" + (inputCleared)="handleInputCleared()" [value]="searchInputValue$ | async" [clearOnSelection]="true" > diff --git a/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.spec.ts b/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.spec.ts index 39328db8d9..b4f31ca72a 100644 --- a/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.spec.ts +++ b/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.spec.ts @@ -109,7 +109,6 @@ describe('FuzzySearchComponent', () => { }) describe('search enter key press', () => { - let outputValue describe('when no output defined', () => { beforeEach(() => { component.handleInputSubmission('blarg') @@ -123,8 +122,7 @@ describe('FuzzySearchComponent', () => { describe('when output is defined', () => { beforeEach(() => { jest.resetAllMocks() - outputValue = null - component.inputSubmitted.subscribe((event) => (outputValue = event)) + component.inputSubmitted.subscribe() jest.spyOn(component.inputSubmitted, 'emit') component.handleInputSubmission('blarg') }) @@ -139,6 +137,25 @@ describe('FuzzySearchComponent', () => { }) }) + describe('search input clear', () => { + describe('when output is defined', () => { + beforeEach(() => { + jest.resetAllMocks() + component.inputSubmitted.subscribe() + jest.spyOn(component.inputSubmitted, 'emit') + component.handleInputCleared() + }) + it('clears the search filters', () => { + expect(searchService.updateFilters).toHaveBeenCalledWith({ + any: '', + }) + }) + it('does not emit inputSubmitted', () => { + expect(component.inputSubmitted.emit).not.toHaveBeenCalled() + }) + }) + }) + describe('search suggestion selection', () => { describe('when no output defined', () => { beforeEach(() => { diff --git a/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.ts b/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.ts index b13e99b905..2e80a32aec 100644 --- a/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.ts +++ b/libs/feature/search/src/lib/fuzzy-search/fuzzy-search.component.ts @@ -72,4 +72,8 @@ export class FuzzySearchComponent implements OnInit { this.searchService.updateFilters({ any }) } } + + handleInputCleared() { + this.searchService.updateFilters({ any: '' }) + } } diff --git a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.spec.ts b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.spec.ts index be2eb5d4b1..7c11ac9daa 100644 --- a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.spec.ts +++ b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.spec.ts @@ -111,34 +111,38 @@ describe('AutocompleteComponent', () => { }) }) describe('when input is not empty', () => { - let anyEmitted + let anyEmitted: boolean + let clearEmitted: boolean let button beforeEach(() => { + anyEmitted = false + clearEmitted = false component.inputRef.nativeElement.value = 'blar' component.inputRef.nativeElement.dispatchEvent(new InputEvent('input')) component.triggerRef.closePanel = jest.fn() - component.inputSubmitted.subscribe((event) => (anyEmitted = event)) + component.inputSubmitted.subscribe(() => (anyEmitted = true)) + component.inputCleared.subscribe(() => (clearEmitted = true)) fixture.detectChanges() button = fixture.debugElement.query(By.css('.clear-btn')) + button.nativeElement.click() }) it('is visible', () => { expect(button).not.toBeNull() }) it('resets the text input', () => { - button.nativeElement.click() expect(component.inputRef.nativeElement.value).toBe('') }) it('sets the text input of the focus', () => { - button.nativeElement.click() expect(document.activeElement).toBe(component.inputRef.nativeElement) }) it('closes the autocomplete panel', () => { - button.nativeElement.click() expect(component.triggerRef.closePanel).toHaveBeenCalled() }) - it('clears search result by emitting empty string', () => { - button.nativeElement.click() - expect(anyEmitted).toEqual('') + it('does not emit an inputSubmitted event', () => { + expect(anyEmitted).toEqual(false) + }) + it('emits an inputCleared event', () => { + expect(clearEmitted).toEqual(true) }) }) }) diff --git a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts index d8c9c25441..2442fe4c83 100644 --- a/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts +++ b/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts @@ -49,6 +49,7 @@ export class AutocompleteComponent @Input() clearOnSelection = false @Output() itemSelected = new EventEmitter() @Output() inputSubmitted = new EventEmitter() + @Output() inputCleared = new EventEmitter() @ViewChild(MatAutocompleteTrigger) triggerRef: MatAutocompleteTrigger @ViewChild(MatAutocomplete) autocomplete: MatAutocomplete @ViewChild('searchInput') inputRef: ElementRef @@ -126,7 +127,7 @@ export class AutocompleteComponent clear(): void { this.inputRef.nativeElement.value = '' - this.inputSubmitted.emit('') + this.inputCleared.emit() this.selectionSubject .pipe(take(1)) .subscribe((selection) => selection && selection.option.deselect()) diff --git a/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html b/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html index c938fe3cc1..439d197378 100644 --- a/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html +++ b/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html @@ -43,7 +43,7 @@ [cdkConnectedOverlayPositions]="overlayPositions" [cdkConnectedOverlayScrollStrategy]="scrollStrategy" [cdkConnectedOverlayFlexibleDimensions]="true" - (backdropClick)="closeOverlay()" + (overlayOutsideClick)="closeOverlay()" (detach)="closeOverlay()" >