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;