diff --git a/README.md b/README.md
index 9e048d4..f2a4145 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,9 @@ with a ` ` tag:
```
+Note that default styles will not be applied to shadow roots.
+Each root node will need to include the styles separately.
+
### With npm
For more advanced configuration, you can install with
diff --git a/index.html b/index.html
index ed91067..4747b25 100644
--- a/index.html
+++ b/index.html
@@ -8,6 +8,7 @@
+
Popover Attribute Polyfill
@@ -22,6 +23,23 @@ Popover Attribute Polyfill
Popover 8 (auto)
Popover 9 (manual)
Popover 10 (manual)
+
+
diff --git a/src/observer.ts b/src/observer.ts
new file mode 100644
index 0000000..25113a1
--- /dev/null
+++ b/src/observer.ts
@@ -0,0 +1,77 @@
+export const popovers = new Set();
+
+const popoversSyncFactory =
+ (method: (value: HTMLElement) => void) => (node: Node) => {
+ if (node instanceof HTMLElement) {
+ if (node.hasAttribute('popover')) {
+ method(node);
+ }
+
+ node.querySelectorAll('[popover]').forEach((popover) => {
+ if (popover instanceof HTMLElement) {
+ method(popover);
+ }
+ });
+ }
+ };
+
+const nodeAddedCallback = popoversSyncFactory(popovers.add.bind(popovers));
+
+const nodeRemovedCallback = popoversSyncFactory(popovers.delete.bind(popovers));
+
+const handleChildListMutation = (mutation: MutationRecord) => {
+ if (mutation.addedNodes.length > 0) {
+ mutation.addedNodes.forEach(nodeAddedCallback);
+ }
+
+ if (mutation.removedNodes.length > 0) {
+ mutation.removedNodes.forEach(nodeRemovedCallback);
+ }
+};
+
+const handlePopoverAttributeMutation = (mutation: MutationRecord) => {
+ if (mutation.target instanceof HTMLElement) {
+ if (mutation.target.hasAttribute('popover')) {
+ popovers.add(mutation.target);
+ } else {
+ popovers.delete(mutation.target);
+ }
+ }
+};
+
+const handleMutation = (mutationList: MutationRecord[]) => {
+ mutationList.forEach((mutation) => {
+ switch (mutation.type) {
+ case 'attributes':
+ switch (mutation.attributeName) {
+ case 'popover':
+ handlePopoverAttributeMutation(mutation);
+ break;
+ }
+ break;
+ case 'childList':
+ handleChildListMutation(mutation);
+ break;
+ }
+ });
+};
+
+const observer = new MutationObserver(handleMutation);
+
+export const observePopoversMutations = (root: Document | ShadowRoot) => {
+ // Documents don't initially trigger childList mutations as opposed
+ // to shadow roots, so we need to manually add the popovers to the set
+ if (root === document) {
+ root.querySelectorAll('[popover]').forEach((popover) => {
+ if (popover instanceof HTMLElement) {
+ popovers.add(popover);
+ }
+ });
+ }
+
+ observer.observe(root, {
+ attributeFilter: ['popover'],
+ childList: true,
+ subtree: true,
+ });
+};
diff --git a/src/popover.ts b/src/popover.ts
index 349b13a..2bc05ec 100644
--- a/src/popover.ts
+++ b/src/popover.ts
@@ -1,3 +1,5 @@
+import { observePopoversMutations, popovers } from './observer.js';
+
export function isSupported() {
return (
typeof HTMLElement !== 'undefined' &&
@@ -9,6 +11,34 @@ export function isSupported() {
const notSupportedMessage =
'Not supported on element that does not have valid popover attribute';
+function patchAttachShadow(callback: (shadowRoot: ShadowRoot) => void) {
+ const originalAttachShadow = Element.prototype.attachShadow;
+ Element.prototype.attachShadow = function (init) {
+ const shadowRoot = originalAttachShadow.call(this, init);
+ callback(shadowRoot);
+ return shadowRoot;
+ };
+}
+
+const closestElement: (selector: string, target: Element) => Element | null = (
+ selector: string,
+ target: Element,
+) => {
+ const found = target.closest(selector);
+
+ if (found) {
+ return found;
+ }
+
+ const root = target.getRootNode();
+
+ if (root === document || !(root instanceof ShadowRoot)) {
+ return null;
+ }
+
+ return closestElement(selector, root.host);
+};
+
export function apply() {
const visibleElements = new WeakSet();
@@ -66,11 +96,17 @@ export function apply() {
},
});
- document.addEventListener('click', (event: Event) => {
+ const onClick = (event: Event) => {
const target = event.target;
if (!(target instanceof Element)) return;
- const doc = target.ownerDocument;
- let effectedPopover: HTMLElement | null = target.closest('[popover]');
+ const root = target.getRootNode();
+ if (root instanceof ShadowRoot) {
+ event.stopPropagation();
+ } else if (!(root instanceof Document)) return;
+ let effectedPopover = closestElement(
+ '[popover]',
+ target,
+ ) as HTMLElement | null;
const button = target.closest(
'[popovertoggletarget],[popoverhidetarget],[popovershowtarget]',
);
@@ -78,7 +114,7 @@ export function apply() {
// Handle Popover triggers
if (isButton && button.hasAttribute('popovershowtarget')) {
- effectedPopover = doc.getElementById(
+ effectedPopover = root.getElementById(
button.getAttribute('popovershowtarget') || '',
);
@@ -90,7 +126,7 @@ export function apply() {
effectedPopover.showPopover();
}
} else if (isButton && button.hasAttribute('popoverhidetarget')) {
- effectedPopover = doc.getElementById(
+ effectedPopover = root.getElementById(
button.getAttribute('popoverhidetarget') || '',
);
@@ -102,7 +138,7 @@ export function apply() {
effectedPopover.hidePopover();
}
} else if (isButton && button.hasAttribute('popovertoggletarget')) {
- effectedPopover = doc.getElementById(
+ effectedPopover = root.getElementById(
button.getAttribute('popovertoggletarget') || '',
);
@@ -116,11 +152,24 @@ export function apply() {
}
// Dismiss open Popovers
- for (const popover of doc.querySelectorAll(
- '[popover="" i].\\:open, [popover=auto i].\\:open',
- )) {
- if (popover instanceof HTMLElement && popover !== effectedPopover)
+ for (const popover of [...popovers]) {
+ if (
+ popover.matches('[popover="" i].\\:open, [popover=auto i].\\:open') &&
+ popover !== effectedPopover
+ )
popover.hidePopover();
}
- });
+ };
+
+ const addOnClickEventListener = (
+ (callback: (event: Event) => void) => (root: Document | ShadowRoot) => {
+ root.addEventListener('click', callback);
+ }
+ )(onClick);
+
+ observePopoversMutations(document);
+ addOnClickEventListener(document);
+
+ patchAttachShadow(observePopoversMutations);
+ patchAttachShadow(addOnClickEventListener);
}
diff --git a/tests/dismiss.spec.ts b/tests/dismiss.spec.ts
index 248b881..604181b 100644
--- a/tests/dismiss.spec.ts
+++ b/tests/dismiss.spec.ts
@@ -16,6 +16,12 @@ test('click dismisses all auto popovers', async ({ page }) => {
await expect(popover9).toBeHidden();
const popover10 = (await page.locator('#popover10')).nth(0);
await expect(popover10).toBeHidden();
+ const shadowedPopover = (await page.locator('#shadowedPopover')).nth(0);
+ await expect(shadowedPopover).toBeHidden();
+ const shadowedNestedPopover = (
+ await page.locator('#shadowedNestedPopover')
+ ).nth(0);
+ await expect(shadowedNestedPopover).toBeHidden();
await page.click('h1');
await expect(popover7).toBeHidden();
diff --git a/tests/triggers.spec.ts b/tests/triggers.spec.ts
index 25e7d3f..1e2ab67 100644
--- a/tests/triggers.spec.ts
+++ b/tests/triggers.spec.ts
@@ -81,3 +81,29 @@ test('clicking button[popovershowtarget=popover5] then button[popoverhidetarget=
await page.click('button[popoverhidetarget=popover5]');
await expect(popover).toBeHidden();
});
+
+test('clicking button[popovertoggletarget=shadowedPopover] should hide open popover in the same (shadow) tree scope', async ({
+ page,
+}) => {
+ const popover = (await page.locator('#shadowedPopover')).nth(0);
+ await expect(popover).toBeHidden();
+ await expect(
+ await popover.evaluate((node) => node.showPopover()),
+ ).toBeUndefined();
+ await expect(popover).toBeVisible();
+ await page.click('button[popovertoggletarget=shadowedPopover]');
+ await expect(popover).toBeHidden();
+});
+
+test('clicking button[popovertoggletarget=shadowedNestedPopover] should hide open nested popover in the same (shadow) tree scope', async ({
+ page,
+}) => {
+ const popover = (await page.locator('#shadowedNestedPopover')).nth(0);
+ await expect(popover).toBeHidden();
+ await expect(
+ await popover.evaluate((node) => node.showPopover()),
+ ).toBeUndefined();
+ await expect(popover).toBeVisible();
+ await page.click('button[popovertoggletarget=shadowedNestedPopover]');
+ await expect(popover).toBeHidden();
+});