diff --git a/packages/web-components/src/components/popover/__tests__/popover-test.js b/packages/web-components/src/components/popover/__tests__/popover-test.js index 39bcc639c5f0..ba3d12ce09a9 100644 --- a/packages/web-components/src/components/popover/__tests__/popover-test.js +++ b/packages/web-components/src/components/popover/__tests__/popover-test.js @@ -310,3 +310,66 @@ describe('cds-popover-content', function () { ).to.be.false; }); }); +describe('cds-popover outside click', () => { + it('does not close when clicking the trigger button', async () => { + const el = await fixture(html` + + + + + `); + + await el.updateComplete; + expect(el.hasAttribute('open')).to.be.true; + + const trigger = el.querySelector('#trigger'); + trigger.click(); + + await el.updateComplete; + expect(el.hasAttribute('open')).to.be.true; + }); + + it('does not close when clicking the popover content', async () => { + const el = await fixture(html` + + + +
Content
+
+
+ `); + + await el.updateComplete; + expect(el.hasAttribute('open')).to.be.true; + + const content = el + .querySelector('cds-popover-content') + .shadowRoot?.querySelector('.cds--popover-content'); + + content.click(); + + await el.updateComplete; + expect(el.hasAttribute('open')).to.be.true; + }); + + it('closes on outside click', async () => { + const el = await fixture(html` +
+ + + + + +
+ `); + + await el.updateComplete; + const popover = el.querySelector('#popover'); + const outside = el.querySelector('#outside'); + expect(popover.hasAttribute('open')).to.be.true; + + outside.click(); + await el.updateComplete; + expect(popover.hasAttribute('open')).to.be.false; + }); +}); diff --git a/packages/web-components/src/components/popover/popover.stories.ts b/packages/web-components/src/components/popover/popover.stories.ts index 50a0f0c7d532..930c6e9f5edd 100644 --- a/packages/web-components/src/components/popover/popover.stories.ts +++ b/packages/web-components/src/components/popover/popover.stories.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import { html } from 'lit'; +import { html, LitElement } from 'lit'; // remove LitElement before merging import './popover'; import './popover-content'; import '../radio-button/index'; @@ -15,6 +15,8 @@ import Checkbox16 from '@carbon/icons/es/checkbox/16.js'; import Settings16 from '@carbon/icons/es/settings/16.js'; import '../checkbox'; import { iconLoader } from '../../globals/internal/icon-loader'; +import { property } from 'lit/decorators.js'; // remove before merging +import '../layer'; // remove before merging import styles from './popover-story.scss?lit'; const sharedArgTypes = { @@ -522,6 +524,178 @@ export const TabTipExperimentalAutoAlign = { }, }; +// everything from here down until const meta = {} should be removed before merging +class PopTest extends LitElement { + static properties = { + isOpen: { type: Boolean }, + }; + private isOpen: boolean = false; + + constructor() { + super(); + this.isOpen = false; + } + + handleClick() { + this.isOpen = !this.isOpen; + } + + render() { + return html` +
+

Popover open: ${this.isOpen}

+ + + +
+

POPOVER

+
+
+
+
+ `; + } +} +customElements.define('pop-test', PopTest); + +// remove before merging +export const Test1ShouldOpenAndClose = { + render: () => html``, +}; + +class MyCard extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this.shadowRoot.innerHTML = ` +
+ + + + + + +
+ `; + } +} + +customElements.define('my-card', MyCard); + +export const Test2ShouldNotCloseOnContentClick = { + render: () => + html` + +
+

Available storage

+

+ This server has 150 GB of block storage remaining. +

+
+
`, +}; + +class MyApp extends LitElement { + toggleButton = () => { + // eslint-disable-next-line no-console + console.log('toggled'); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const myElement = this.shadowRoot!.querySelector('my-element'); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + myElement!.open = !myElement?.open; + }; + + render() { + return html` + + + +
+ +

Available storage

+

+ This server has 150 GB of block storage remaining. +

+
+
+
+ `; + } +} +customElements.define('my-app', MyApp); + +class MyElement extends LitElement { + /** + * Copy for the read the docs hint. + */ + @property() + open = true; + + updated() { + // eslint-disable-next-line no-console + console.log('element open: ', this.open); + } + + render() { + return html` + + + +
+
+
+ `; + } +} +customElements.define('my-element', MyElement); + +export const Test3ShouldOpenAndClose = { + render: () => html` `, +}; +// stop here + const meta = { title: 'Components/Popover', }; diff --git a/packages/web-components/src/components/popover/popover.ts b/packages/web-components/src/components/popover/popover.ts index 1c42d45c595c..8949468e0eb0 100644 --- a/packages/web-components/src/components/popover/popover.ts +++ b/packages/web-components/src/components/popover/popover.ts @@ -141,6 +141,18 @@ class CDSPopover extends HostListenerMixin(LitElement) { } private _handleOutsideClick(event: Event) { + const path = event.composedPath(); + + if (path.includes(this._triggerSlotNode.assignedElements()[0])) return; + + const popoverContent = this.querySelector( + (this.constructor as typeof CDSPopover).selectorPopoverContent + )?.shadowRoot?.querySelector( + (this.constructor as typeof CDSPopover).selectorPopoverContentClass + ) as HTMLElement; + + if (path.includes(popoverContent)) return; + const target = event.target as Node | null; const composedTarget = event.composedPath?.()[0] as Node | null;