Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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`
<cds-popover open>
<button id="trigger" type="button">Test</button>
<cds-popover-content></cds-popover-content>
</cds-popover>
`);

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`
<cds-popover open>
<button type="button">Test</button>
<cds-popover-content>
<div>Content</div>
</cds-popover-content>
</cds-popover>
`);

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`
<div>
<cds-popover open id="popover">
<button type="button">Test</button>
<cds-popover-content></cds-popover-content>
</cds-popover>
<button id="outside"></button>
</div>
`);

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;
});
});
176 changes: 175 additions & 1 deletion packages/web-components/src/components/popover/popover.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 = {
Expand Down Expand Up @@ -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`
<div>
<h3>Popover open: ${this.isOpen}</h3>
<cds-popover ?open=${this.isOpen}>
<button @click="${this.handleClick}">PRESS ME</button>
<cds-popover-content>
<div>
<h3>POPOVER</h3>
</div>
</cds-popover-content>
</cds-popover>
</div>
`;
}
}
customElements.define('pop-test', PopTest);

// remove before merging
export const Test1ShouldOpenAndClose = {
render: () => html`<pop-test></pop-test>`,
};

class MyCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<div>
<cds-popover open="" caret="" align="bottom" dropshadow="" backgroundtoken="">
<slot name="popover-trigger"></slot>
<cds-popover-content>
<slot name="popover-content"></slot>
</cds-popover-content>
</cds-popover>
</div>
`;
}
}

customElements.define('my-card', MyCard);

export const Test2ShouldNotCloseOnContentClick = {
render: () =>
html`<my-card>
<button
slot="popover-trigger"
class="playground-trigger"
aria-label="Checkbox"
type="button"
aria-expanded="function open() { [native code] }">
<svg
focusable="false"
preserveAspectRatio="xMidYMid meet"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
width="16"
height="16"
viewBox="0 0 32 32"
aria-hidden="true">
<path
d="M26,4H6A2,2,0,0,0,4,6V26a2,2,0,0,0,2,2H26a2,2,0,0,0,2-2V6A2,2,0,0,0,26,4ZM6,26V6H26V26Z"></path>
</svg>
</button>
<div slot="popover-content" class="p-3">
<p class="popover-title">Available storage</p>
<p class="popover-details">
This server has 150 GB of block storage remaining.
</p>
</div>
</my-card>`,
};

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`
<my-element>
<button
aria-label="Settings"
type="button"
slot="trigger"
@click=${this.toggleButton}>
<svg
focusable="false"
preserveAspectRatio="xMidYMid meet"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
width="16"
height="16"
viewBox="0 0 16 16"
aria-hidden="true">
<path
d="M13.5,8.4c0-0.1,0-0.3,0-0.4c0-0.1,0-0.3,0-0.4l1-0.8c0.4-0.3,0.4-0.9,0.2-1.3l-1.2-2C13.3,3.2,13,3,12.6,3 c-0.1,0-0.2,0-0.3,0.1l-1.2,0.4c-0.2-0.1-0.4-0.3-0.7-0.4l-0.3-1.3C10.1,1.3,9.7,1,9.2,1H6.8c-0.5,0-0.9,0.3-1,0.8L5.6,3.1 C5.3,3.2,5.1,3.3,4.9,3.4L3.7,3C3.6,3,3.5,3,3.4,3C3,3,2.7,3.2,2.5,3.5l-1.2,2C1.1,5.9,1.2,6.4,1.6,6.8l0.9,0.9c0,0.1,0,0.3,0,0.4 c0,0.1,0,0.3,0,0.4L1.6,9.2c-0.4,0.3-0.5,0.9-0.2,1.3l1.2,2C2.7,12.8,3,13,3.4,13c0.1,0,0.2,0,0.3-0.1l1.2-0.4 c0.2,0.1,0.4,0.3,0.7,0.4l0.3,1.3c0.1,0.5,0.5,0.8,1,0.8h2.4c0.5,0,0.9-0.3,1-0.8l0.3-1.3c0.2-0.1,0.4-0.2,0.7-0.4l1.2,0.4 c0.1,0,0.2,0.1,0.3,0.1c0.4,0,0.7-0.2,0.9-0.5l1.1-2c0.2-0.4,0.2-0.9-0.2-1.3L13.5,8.4z M12.6,12l-1.7-0.6c-0.4,0.3-0.9,0.6-1.4,0.8 L9.2,14H6.8l-0.4-1.8c-0.5-0.2-0.9-0.5-1.4-0.8L3.4,12l-1.2-2l1.4-1.2c-0.1-0.5-0.1-1.1,0-1.6L2.2,6l1.2-2l1.7,0.6 C5.5,4.2,6,4,6.5,3.8L6.8,2h2.4l0.4,1.8c0.5,0.2,0.9,0.5,1.4,0.8L12.6,4l1.2,2l-1.4,1.2c0.1,0.5,0.1,1.1,0,1.6l1.4,1.2L12.6,12z"></path>
<path
d="M8,11c-1.7,0-3-1.3-3-3s1.3-3,3-3s3,1.3,3,3C11,9.6,9.7,11,8,11C8,11,8,11,8,11z M8,6C6.9,6,6,6.8,6,7.9C6,7.9,6,8,6,8 c0,1.1,0.8,2,1.9,2c0,0,0.1,0,0.1,0c1.1,0,2-0.8,2-1.9c0,0,0-0.1,0-0.1C10,6.9,9.2,6,8,6C8.1,6,8,6,8,6z"></path>
</svg>
</button>

<div slot="content">
<cds-layer>
<p class="popover-title">Available storage</p>
<p class="popover-details">
This server has 150 GB of block storage remaining.
</p>
</cds-layer>
</div>
</my-element>
`;
}
}
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`
<cds-popover
tabtip=""
align="bottom-left"
id="popover-one"
dropshadow=""
?open=${this.open}>
<slot name="trigger"></slot>
<cds-popover-content>
<div><slot name="content"></slot></div>
</cds-popover-content>
</cds-popover>
`;
}
}
customElements.define('my-element', MyElement);

export const Test3ShouldOpenAndClose = {
render: () => html` <my-app> </my-app> `,
};
// stop here

const meta = {
title: 'Components/Popover',
};
Expand Down
12 changes: 12 additions & 0 deletions packages/web-components/src/components/popover/popover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading