diff --git a/libs/core/popover/base/base-popover.class.ts b/libs/core/popover/base/base-popover.class.ts index 8c50a6449de..476e193114f 100644 --- a/libs/core/popover/base/base-popover.class.ts +++ b/libs/core/popover/base/base-popover.class.ts @@ -92,6 +92,10 @@ export class BasePopoverClass { @Input() closeOnOutsideClick = true; + /** Wether to apply a background overlay */ + @Input() + applyOverlay = false; + /** Whether the popover should be focusTrapped. */ @Input() focusTrapped = false; diff --git a/libs/core/popover/popover-body/popover-body.component.ts b/libs/core/popover/popover-body/popover-body.component.ts index 47419d11101..b441f56af3a 100644 --- a/libs/core/popover/popover-body/popover-body.component.ts +++ b/libs/core/popover/popover-body/popover-body.component.ts @@ -142,6 +142,16 @@ export class PopoverBodyComponent implements AfterViewInit { } } + /** Handler for focus when clicking outside */ + @HostListener('document:click', ['$event.target']) + onClick(targetElement: HTMLElement): void { + const clickedInside = this._elementRef.nativeElement.contains(targetElement); + if (!clickedInside) { + // Call the focus logic if clicked outside the popover + this._focusFirstTabbableElement(); + } + } + /** @hidden */ ngAfterViewInit(): void { if (this._scrollbar) { diff --git a/libs/core/popover/popover-service/popover.service.spec.ts b/libs/core/popover/popover-service/popover.service.spec.ts index 32b88fe05f4..1cd0fa2eb19 100644 --- a/libs/core/popover/popover-service/popover.service.spec.ts +++ b/libs/core/popover/popover-service/popover.service.spec.ts @@ -247,8 +247,9 @@ describe('PopoverService', () => { expect((service)._shouldClose(mouseEvent)).not.toEqual(true); }); - it('shouldn close on escape keydown from popover body', () => { + it("shouldn't close on closeOnOutsideClick from popover body", () => { service.initialise(componentInstance.triggerRef, componentInstance, componentInstance.getPopoverTemplateData()); + service.closeOnOutsideClick = false; service.open(); @@ -256,9 +257,40 @@ describe('PopoverService', () => { jest.spyOn(service, 'close'); - componentInstance.popoverBody.onClose.next(); + document.body.click(); - expect(service.close).toHaveBeenCalled(); + expect(service.close).not.toHaveBeenCalled(); + + }); + + it ("shouldn't close on escape keydown from popover body", () => { + service.initialise(componentInstance.triggerRef, componentInstance, componentInstance.getPopoverTemplateData()); + service.closeOnEscapeKey = false; + + service.open(); + + fixture.detectChanges(); + + jest.spyOn(service, 'close'); + + const event = new KeyboardEvent('keydown', { key: 'Escape' }); + document.dispatchEvent(event); + + expect(service.close).not.toHaveBeenCalled(); + }); + + it("should contain the appropriate classes when checkModalBackground is called", () => { + service.initialise(componentInstance.triggerRef, componentInstance, componentInstance.getPopoverTemplateData()); + service.closeOnOutsideClick = false; + + service.open(); + + service.checkModalBackground(); + + fixture.detectChanges(); + + expect(document.body.classList.contains('fd-overlay-active')).toBe(true); + expect(document.querySelector('.fd-popover__modal')).toBeTruthy(); }); it('should resize overlay body at least, on refresh position', () => { diff --git a/libs/core/popover/popover-service/popover.service.ts b/libs/core/popover/popover-service/popover.service.ts index b59dca701a6..9b2f6ce6c39 100644 --- a/libs/core/popover/popover-service/popover.service.ts +++ b/libs/core/popover/popover-service/popover.service.ts @@ -85,6 +85,15 @@ export class PopoverService extends BasePopoverClass { /** @hidden */ private _ignoreTriggers = false; + /** @hidden */ + private _modalBodyClass = 'fd-overlay-active'; + + /** @hidden */ + private _modalTriggerClass = 'fd-popover__modal'; + + /** @hidden */ + private _isModal = false; + /** An RxJS Subject that will kill the data stream upon component’s destruction (for unsubscribing) */ private readonly _destroyRef = inject(DestroyRef); @@ -113,6 +122,10 @@ export class PopoverService extends BasePopoverClass { this._overlayRef.detach(); this._overlayRef.dispose(); } + + if (this._isModal) { + this._removeOverlay(this._modalBodyClass, this._modalTriggerClass); + } }); } @@ -165,6 +178,7 @@ export class PopoverService extends BasePopoverClass { this.isOpenChange.emit(this.isOpen); } + this.checkModalBackground(); this._focusLastActiveElementBeforeOpen(focusActiveElement); } @@ -230,12 +244,24 @@ export class PopoverService extends BasePopoverClass { } } + /** Changes background theming when modal */ + /** @hidden */ + checkModalBackground(): void { + const isClosingConditions = (!this.closeOnOutsideClick || !this.closeOnEscapeKey) && this.applyOverlay; + if (isClosingConditions && this.isOpen) { + this._addModalOverlay(this._modalBodyClass, this._modalTriggerClass); + } else if (isClosingConditions && !this.isOpen) { + this._removeOverlay(this._modalBodyClass, this._modalTriggerClass); + } + } + /** Toggles the popover open state */ toggle(openAction = true, closeAction = true): void { if (this.isOpen) { closeAction && this.close(); } else { openAction && this.open(); + this.checkModalBackground(); } } @@ -404,6 +430,18 @@ export class PopoverService extends BasePopoverClass { this._eventRef = []; } + private _addModalOverlay(bodyClass: string, triggerClass: string): void { + this._renderer.addClass(document.body, bodyClass); + this._renderer.addClass((this._triggerElement as ElementRef).nativeElement, triggerClass); + this._isModal = true; + } + + private _removeOverlay(bodyClass: string, triggerClass: string): void { + this._renderer.removeClass(document.body, bodyClass); + this._renderer.removeClass((this._triggerElement as ElementRef).nativeElement, triggerClass); + this._isModal = false; + } + /** Attach template containing popover body to overlay */ private _attachTemplate(): void { this._passVariablesToBody(); diff --git a/libs/docs/core/popover/examples/popover-closing-example/popover-closing-example.component.html b/libs/docs/core/popover/examples/popover-closing-example/popover-closing-example.component.html new file mode 100644 index 00000000000..e2b5cda9b47 --- /dev/null +++ b/libs/docs/core/popover/examples/popover-closing-example/popover-closing-example.component.html @@ -0,0 +1,69 @@ +
+ + + + + +
+
+
+ +
Header
+
+
+
+ +
+
+ + + + + +
+
+
+ +
Header
+
+
+
+ +
+
+ + + + + +
+
+
+ +
Header
+
+
+
+ +
+
+
diff --git a/libs/docs/core/popover/examples/popover-closing-example/popover-closing-example.component.scss b/libs/docs/core/popover/examples/popover-closing-example/popover-closing-example.component.scss new file mode 100644 index 00000000000..ad972460685 --- /dev/null +++ b/libs/docs/core/popover/examples/popover-closing-example/popover-closing-example.component.scss @@ -0,0 +1,7 @@ +.fd-docs-flex-display-helper { + display: flex; + align-items: center; + justify-content: space-around; + flex-flow: row wrap; + width: 100%; +} diff --git a/libs/docs/core/popover/examples/popover-closing-example/popover-closing-example.component.ts b/libs/docs/core/popover/examples/popover-closing-example/popover-closing-example.component.ts new file mode 100644 index 00000000000..78cc92bc2d6 --- /dev/null +++ b/libs/docs/core/popover/examples/popover-closing-example/popover-closing-example.component.ts @@ -0,0 +1,42 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { AvatarComponent } from '@fundamental-ngx/core/avatar'; +import { + BarComponent, + BarElementDirective, + BarLeftDirective, + BarMiddleDirective, + BarRightDirective, + ButtonBarComponent +} from '@fundamental-ngx/core/bar'; +import { ButtonComponent } from '@fundamental-ngx/core/button'; +import { + PopoverBodyComponent, + PopoverBodyDirective, + PopoverBodyHeaderDirective, + PopoverComponent, + PopoverControlComponent +} from '@fundamental-ngx/core/popover'; + +@Component({ + selector: 'fd-popover-closing-example', + templateUrl: './popover-closing-example.component.html', + styleUrls: ['./popover-closing-example.component.scss'], + standalone: true, + encapsulation: ViewEncapsulation.None, + imports: [ + PopoverComponent, + PopoverControlComponent, + PopoverBodyComponent, + ButtonComponent, + PopoverBodyHeaderDirective, + PopoverBodyDirective, + AvatarComponent, + BarComponent, + ButtonBarComponent, + BarElementDirective, + BarLeftDirective, + BarMiddleDirective, + BarRightDirective + ] +}) +export class PopoverClosingExampleComponent {} diff --git a/libs/docs/core/popover/popover-docs.component.html b/libs/docs/core/popover/popover-docs.component.html index 52083ac477f..7e88698240c 100644 --- a/libs/docs/core/popover/popover-docs.component.html +++ b/libs/docs/core/popover/popover-docs.component.html @@ -10,6 +10,20 @@ + Closing Popovers + + Popovers can be closed in a variety of ways. By default, popovers close when the user clicks outside of the popover + or presses the escape key. These behaviors can be controlled by setting the closeOnOutsideClick and + closeOnEscapeKey inputs to false. Popovers can also be closed when the user navigates away + from the page by setting the closeOnNavigation input to true. + + + + + + + + Simple Popover There is different way to build popover, it can be done, by using simplified markup, with trigger element connected diff --git a/libs/docs/core/popover/popover-docs.component.ts b/libs/docs/core/popover/popover-docs.component.ts index 4e87df93807..93183955e91 100644 --- a/libs/docs/core/popover/popover-docs.component.ts +++ b/libs/docs/core/popover/popover-docs.component.ts @@ -11,6 +11,7 @@ import { getAssetFromModuleAssets } from '@fundamental-ngx/docs/shared'; import { PopoverCFillComponent } from './examples/popover-c-fill/popover-c-fill.component'; +import { PopoverClosingExampleComponent } from './examples/popover-closing-example/popover-closing-example.component'; import { PopoverComplexExampleComponent } from './examples/popover-complex-example/popover-complex-example.component'; import { PopoverContainerExampleComponent } from './examples/popover-container-example/popover-container-example.component'; import { PopoverDialogExampleComponent } from './examples/popover-dialog/popover-dialog-example.component'; @@ -34,6 +35,8 @@ const dropdownPopoverScss = 'popover-dropdown/popover-dropdown.component.scss'; const popoverSrc = 'popover-simple/popover-example.component.html'; const popoverSrcTs = 'popover-simple/popover-example.component.ts'; +const popoverClosingSrc = 'popover-closing-example/popover-closing-example.component.html'; +const popoverClosingSrcTs = 'popover-closing-example/popover-closing-example.component.ts'; const popoverComplexSrc = 'popover-complex-example/popover-complex-example.component.html'; const popoverComplexSrcTs = 'popover-complex-example/popover-complex-example.component.ts'; const popoverProgrammaticHtmlSrc = 'popover-programmatic/popover-programmatic-open-example.component.html'; @@ -81,6 +84,7 @@ const dynamicContainerHeightTsSrc = DescriptionComponent, ComponentExampleComponent, PopoverExampleComponent, + PopoverClosingExampleComponent, CodeExampleComponent, SeparatorComponent, PopoverTriggerExampleComponent, @@ -114,6 +118,21 @@ export class PopoverDocsComponent { } ]; + popoverClosingExample: ExampleFile[] = [ + { + language: 'html', + code: getAssetFromModuleAssets(popoverClosingSrc), + fileName: 'popover-closing-example' + }, + { + language: 'typescript', + code: getAssetFromModuleAssets(popoverTriggerSrcTs), + fileName: 'popover-closing-exampl', + typescriptFileCode: getAssetFromModuleAssets(popoverClosingSrcTs), + component: 'PopoverClosingExampleComponent' + } + ]; + popoverComplex: ExampleFile[] = [ { language: 'html', diff --git a/package.json b/package.json index 055504ce25e..ceead147f77 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@angular/platform-browser": "18.0.3", "@angular/platform-browser-dynamic": "18.0.3", "@angular/router": "18.0.3", - "@fundamental-styles/cx": "0.37.7", + "@fundamental-styles/cx": "0.37.8", "@nx/angular": "19.2.3", "@sap-theming/theming-base-content": "11.18.0", "@stackblitz/sdk": "1.9.0", @@ -58,7 +58,7 @@ "fast-deep-equal": "3.1.3", "focus-trap": "7.1.0", "focus-visible": "5.2.1", - "fundamental-styles": "0.37.7", + "fundamental-styles": "0.37.8", "fuse.js": "7.0.0", "highlight.js": "11.7.0", "intl": "1.2.5", @@ -92,7 +92,7 @@ "@nx/js": "19.2.3", "@nx/plugin": "19.2.3", "@nx/workspace": "19.2.3", - "@sap-ui/common-css": "0.37.7", + "@sap-ui/common-css": "0.37.8", "@schematics/angular": "18.0.4", "@swc-node/register": "1.9.2", "@swc/cli": "0.3.12", diff --git a/yarn.lock b/yarn.lock index 431b55c3d52..f30b843ba0b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3585,10 +3585,10 @@ __metadata: languageName: node linkType: hard -"@fundamental-styles/cx@npm:0.37.7": - version: 0.37.7 - resolution: "@fundamental-styles/cx@npm:0.37.7" - checksum: 10/755cba4a65cb68a68bcfd9f589f2a7607e6ea736f18abcdfed7a3cc9bafbcc1248ba5f4b4c3b62dca99591c7b5c3791d546a70c122f1c303ac9ed5351aec5b29 +"@fundamental-styles/cx@npm:0.37.8": + version: 0.37.8 + resolution: "@fundamental-styles/cx@npm:0.37.8" + checksum: 10/6afeae5648d5fd799a80dc604839415109f6afeea051dd0f6401eb82089b041eb223ac1ab6771b687ba222b7db8842e51977cb06d60597c84323cd8882c98392 languageName: node linkType: hard @@ -5338,12 +5338,12 @@ __metadata: languageName: node linkType: hard -"@sap-ui/common-css@npm:0.37.7": - version: 0.37.7 - resolution: "@sap-ui/common-css@npm:0.37.7" +"@sap-ui/common-css@npm:0.37.8": + version: 0.37.8 + resolution: "@sap-ui/common-css@npm:0.37.8" dependencies: "@sap-theming/theming-base-content": "npm:^11.18.0" - checksum: 10/2d51b8878a7ce70dedf13154d1a2eca035aaf5856468908f9de16560ac50d0b6e537c85f125b52cdf1fff7aac6ff0ab7b648ec326fee27112df8625c5e2edc6c + checksum: 10/7a517c69ecf50e9b39225d00f44e3795f43045bc9b02d7918a5b1dd710f9ea5e3fbb5738da4eb4a1038ab21919a32063089e04e5c87b8f6b71726fac33268e53 languageName: node linkType: hard @@ -13465,7 +13465,7 @@ __metadata: "@angular/router": "npm:18.0.3" "@commitlint/cli": "npm:18.6.1" "@commitlint/config-conventional": "npm:18.6.1" - "@fundamental-styles/cx": "npm:0.37.7" + "@fundamental-styles/cx": "npm:0.37.8" "@jsdevtools/npm-publish": "npm:3.0.1" "@nx/angular": "npm:19.2.3" "@nx/devkit": "npm:19.2.3" @@ -13476,7 +13476,7 @@ __metadata: "@nx/plugin": "npm:19.2.3" "@nx/workspace": "npm:19.2.3" "@sap-theming/theming-base-content": "npm:11.18.0" - "@sap-ui/common-css": "npm:0.37.7" + "@sap-ui/common-css": "npm:0.37.8" "@schematics/angular": "npm:18.0.4" "@stackblitz/sdk": "npm:1.9.0" "@swc-node/register": "npm:1.9.2" @@ -13522,7 +13522,7 @@ __metadata: fast-glob: "npm:3.3.1" focus-trap: "npm:7.1.0" focus-visible: "npm:5.2.1" - fundamental-styles: "npm:0.37.7" + fundamental-styles: "npm:0.37.8" fuse.js: "npm:7.0.0" highlight.js: "npm:11.7.0" husky: "npm:8.0.2" @@ -13569,13 +13569,13 @@ __metadata: languageName: unknown linkType: soft -"fundamental-styles@npm:0.37.7": - version: 0.37.7 - resolution: "fundamental-styles@npm:0.37.7" +"fundamental-styles@npm:0.37.8": + version: 0.37.8 + resolution: "fundamental-styles@npm:0.37.8" peerDependencies: "@sap-theming/theming-base-content": ^11.18.0 - "@sap-ui/common-css": 0.37.7 - checksum: 10/0d33001fa1a6e7aefdc81a2c316c25139875452c64cd81c564357dbba06b4a5d8b1623f61e7918343b1dc4a08d6e725b61fda0ac79e7e57c1b2b8fe9ef4e5b80 + "@sap-ui/common-css": 0.37.8 + checksum: 10/d387a44e7faac9f5f963ef8f1d2f01b01a9263797b78689b63501260c0f287364db07c37b1650cd2fe855826914471b8c83d9ea4651f87ae6883b574b4b97f49 languageName: node linkType: hard