Skip to content

Commit

Permalink
Merge pull request #49 from yinonov/Using-popover-inside-shadowRoot-#44
Browse files Browse the repository at this point in the history
  • Loading branch information
jgerigmeyer authored Jan 14, 2023
2 parents 2d3625f + f0c89af commit 008e805
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 11 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ with a `<link rel=stylesheet>` tag:
<link rel="stylesheet" src="/path/to/popover.css" />
```

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
Expand Down
18 changes: 18 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<script src="./dist/popover.js"></script>
<link rel="stylesheet" href="./dist/popover.css" />
</head>

<body>
<h1>Popover Attribute Polyfill</h1>

Expand All @@ -22,6 +23,23 @@ <h1>Popover Attribute Polyfill</h1>
<div id="popover8" popover="auto">Popover 8 (auto)</div>
<div id="popover9" popover="manual">Popover 9 (manual)</div>
<div id="popover10" popover="manual">Popover 10 (manual)</div>
<div id="host"></div>
<script type="module">
import sheet from './dist/popover.css' assert { type: 'css' };
const host = document.getElementById('host');
const shadowRoot = host.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `<button popovertoggletarget="shadowedPopover">
Click to toggle shadowed Popover (auto)
</button>
<button popovertoggletarget="shadowedNestedPopover">
Click to toggle shadowed nested Popover (auto)
</button>
<div id="shadowedPopover" popover="auto">Shadowed Popover (auto)</div>
<div>
<div id="shadowedNestedPopover" popover="auto">Shadowed Nested Popover (auto)</div>
</div>`;
shadowRoot.adoptedStyleSheets = [sheet];
</script>
</div>

<div id="buttons">
Expand Down
77 changes: 77 additions & 0 deletions src/observer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
export const popovers = new Set<HTMLElement>();

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,
});
};
71 changes: 60 additions & 11 deletions src/popover.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { observePopoversMutations, popovers } from './observer.js';

export function isSupported() {
return (
typeof HTMLElement !== 'undefined' &&
Expand All @@ -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<HTMLElement>();

Expand Down Expand Up @@ -66,19 +96,25 @@ 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]',
);
const isButton = button instanceof HTMLButtonElement;

// Handle Popover triggers
if (isButton && button.hasAttribute('popovershowtarget')) {
effectedPopover = doc.getElementById(
effectedPopover = root.getElementById(
button.getAttribute('popovershowtarget') || '',
);

Expand All @@ -90,7 +126,7 @@ export function apply() {
effectedPopover.showPopover();
}
} else if (isButton && button.hasAttribute('popoverhidetarget')) {
effectedPopover = doc.getElementById(
effectedPopover = root.getElementById(
button.getAttribute('popoverhidetarget') || '',
);

Expand All @@ -102,7 +138,7 @@ export function apply() {
effectedPopover.hidePopover();
}
} else if (isButton && button.hasAttribute('popovertoggletarget')) {
effectedPopover = doc.getElementById(
effectedPopover = root.getElementById(
button.getAttribute('popovertoggletarget') || '',
);

Expand All @@ -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);
}
6 changes: 6 additions & 0 deletions tests/dismiss.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
26 changes: 26 additions & 0 deletions tests/triggers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

0 comments on commit 008e805

Please sign in to comment.