Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(popover): support shadow tree elements lookup #49

Merged
merged 58 commits into from
Jan 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
2a239c8
fix(popover): support shadow tree elements lookup
Dec 20, 2022
614cd00
refactor closest as well
Dec 20, 2022
363c9a4
code style
Dec 20, 2022
29be951
Merge branch 'main' into Using-popover-inside-shadowRoot-#44
yinonov Dec 22, 2022
4c415af
patching attachShadow
Dec 22, 2022
6864990
resolve conflicts
Dec 22, 2022
de40107
update lock file
Dec 22, 2022
564ecbe
shadow observar works
Dec 23, 2022
b824ad0
comment out class temporarily
Dec 23, 2022
77732e1
remove query-selector-shadow-dom
Dec 23, 2022
7693d58
cover removed nodes and popover attribute mutations
Dec 25, 2022
02f5555
organize code
Dec 25, 2022
ef3f7d7
expose popovers set
Dec 25, 2022
87fc1a0
add file extension
Dec 25, 2022
754ff45
suppoort document
Dec 25, 2022
65175ba
refactor composedPath to custom closest fn
Dec 25, 2022
663c9fb
rename and style
Dec 26, 2022
b2415e8
added a test
Jan 1, 2023
92a56bb
Merge branch 'main' into Using-popover-inside-shadowRoot-#44
yinonov Jan 1, 2023
a38a512
Update tests/dismiss.spec.ts
yinonov Jan 1, 2023
8bac7a2
add
Jan 1, 2023
2828790
fix popover query to filter manual popovers
Jan 1, 2023
3b0e8b7
caveat
Jan 2, 2023
4378131
repwrite caveats
Jan 2, 2023
ac754dc
style custom element
Jan 2, 2023
65bc080
findEffectedPopover
Jan 2, 2023
d020168
triggering elements caveat
Jan 2, 2023
1542bf4
format
Jan 2, 2023
eae2e28
revert
Jan 3, 2023
37c852d
revert
Jan 3, 2023
196a05a
Merge branch 'main' into Using-popover-inside-shadowRoot-#44
yinonov Jan 4, 2023
3a771c1
support in tree trigerring elements
Jan 4, 2023
0843301
rename and ergonomics
Jan 4, 2023
9255ba2
narrow down caveats
Jan 4, 2023
9d25d0f
simpler markup
Jan 4, 2023
7e303a2
ORIGINAL_ATTACH_SHADOW_SYMBOL
Jan 4, 2023
2bf0ebb
clean up
Jan 4, 2023
308535a
Update src/popover.ts
yinonov Jan 4, 2023
60deec0
simple monkey patch
Jan 4, 2023
edfd381
merge
Jan 4, 2023
d131ee2
multi use of patching
Jan 4, 2023
fe4ba1a
test trigger in shadow
Jan 4, 2023
3680d31
shadowedPopover
Jan 4, 2023
a88a114
Update README.md
yinonov Jan 4, 2023
2da9827
Update README.md
yinonov Jan 4, 2023
853dcb3
sync ChildListMutation nested popovers
Jan 11, 2023
9a8adea
Merge branch 'main' into Using-popover-inside-shadowRoot-#44
yinonov Jan 11, 2023
4d03147
add nested test
Jan 11, 2023
697d409
correct tests definitions
Jan 11, 2023
d78777f
testing against open shadowRoot
Jan 12, 2023
1daa16b
rephrase doc
Jan 12, 2023
83f8690
revert webServer comment
Jan 12, 2023
95e27a1
code style
Jan 12, 2023
b2bb43a
Merge branch 'main' into Using-popover-inside-shadowRoot-#44
jgerigmeyer Jan 13, 2023
3700089
Update index.html
yinonov Jan 13, 2023
63bd379
Update README.md
yinonov Jan 13, 2023
3e796a4
Update src/observar.ts
yinonov Jan 13, 2023
f0c89af
filename typo
Jan 13, 2023
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
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();
});