From 3e23d5c42bf36708b021c3dd82ea12ee23836e5b Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Tue, 14 Mar 2023 12:31:22 +0000 Subject: [PATCH 1/4] showing auto popover hides all other popovers --- src/data.ts | 3 ++ src/popover-helpers.ts | 53 ++++++++++++++++++- src/popover.ts | 116 +++++++++++++++-------------------------- tests/dismiss.spec.ts | 17 ++++++ 4 files changed, 114 insertions(+), 75 deletions(-) diff --git a/src/data.ts b/src/data.ts index f527e86..4de1b1a 100644 --- a/src/data.ts +++ b/src/data.ts @@ -20,3 +20,6 @@ export const popoverInvokerSelector = popoverInvokerSupportedElements return popoverInvokerAttributes.map((a) => `${s}[${a}]`); }) .join(', '); + +export const openPopoverSelector = + '[popover="" i].\\:open, [popover=auto i].\\:open'; diff --git a/src/popover-helpers.ts b/src/popover-helpers.ts index a33f028..30d21c7 100644 --- a/src/popover-helpers.ts +++ b/src/popover-helpers.ts @@ -1,4 +1,9 @@ -import { invokers, popoverInvokerSelector, popovers } from './data.js'; +import { + invokers, + openPopoverSelector, + popoverInvokerSelector, + popovers, +} from './data.js'; export const initialAriaExpandedValue = new WeakMap< HTMLButtonElement | HTMLInputElement, null | string @@ -19,6 +24,52 @@ export function getPopoverFor(el: HTMLButtonElement | HTMLInputElement) { ); } +export function* getOpenAutoPopovers() { + for (const popover of popovers) { + if (popover.matches(openPopoverSelector)) { + yield popover; + } + } +} + +export function hideOpenAutoPopovers(except?: Element) { + for (const popover of getOpenAutoPopovers()) { + if (popover !== except) popover.hidePopover(); + } +} + +export function closestShadowPenetrating( + selector: string, + target: Element, +): Element | undefined { + const found = target.closest(selector); + if (found) { + return found; + } + + const root = target.getRootNode(); + if (root === document || !(root instanceof ShadowRoot)) { + return; + } + + return closestShadowPenetrating(selector, root.host); +} + +export function hasPopoverAncestor( + element: Element, + popover: Element, +): boolean { + let parent = element; + do { + const ancestor = closestShadowPenetrating('[popover]', parent); + if (!ancestor) return false; + if (ancestor === popover) return true; + parent = + ancestor.parentElement || (ancestor.getRootNode() as ShadowRoot)?.host; + } while (parent); + return false; +} + export function setInvokerAriaExpanded( el: HTMLButtonElement | HTMLInputElement, ) { diff --git a/src/popover.ts b/src/popover.ts index 2a64ee0..0ab1400 100644 --- a/src/popover.ts +++ b/src/popover.ts @@ -1,6 +1,11 @@ -import { popoverInvokerSelector, popovers } from './data.js'; +import { openPopoverSelector, popoverInvokerSelector } from './data.js'; import { observePopoversMutations } from './observer.js'; -import { getInvokersFor, setInvokerAriaExpanded } from './popover-helpers.js'; +import { + closestShadowPenetrating, + getInvokersFor, + hideOpenAutoPopovers, + setInvokerAriaExpanded, +} from './popover-helpers.js'; export function isSupported() { return ( @@ -19,38 +24,6 @@ function patchAttachShadow(callback: (shadowRoot: ShadowRoot) => void) { }; } -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); -}; - -const queryAncestorAll = ( - element: Element, - selector: string, - popovers: Element[] = [], -): Element[] => { - const ancestor = closestElement(selector, element); - const parent = - ancestor?.parentElement || (ancestor?.getRootNode() as ShadowRoot)?.host; - return ancestor && parent - ? queryAncestorAll(parent, selector, [ancestor, ...popovers]) - : popovers; -}; - export function apply() { const visibleElements = new WeakSet(); let lastFocusedElement: HTMLElement | null = null; @@ -149,6 +122,7 @@ export function apply() { for (const invoker of getInvokersFor(this)) { setInvokerAriaExpanded(invoker); } + hideOpenAutoPopovers(this); } }, }, @@ -246,33 +220,6 @@ export function apply() { definePopoverTargetElementProperty('popoverShowTarget'); definePopoverTargetElementProperty('popoverHideTarget'); - const handlePopoverTargetElementInvocation = (invoker: Element | null) => { - if ( - !(invoker instanceof HTMLButtonElement) && - !(invoker instanceof HTMLInputElement) - ) { - return; - } - let popoverTargetElement: HTMLElement | null = null; - if (invoker.popoverToggleTargetElement) { - popoverTargetElement = invoker.popoverToggleTargetElement; - if (popoverTargetElement) { - if (visibleElements.has(popoverTargetElement)) { - popoverTargetElement.hidePopover(); - } else { - popoverTargetElement.showPopover(); - } - } - } else if (invoker.popoverShowTargetElement) { - popoverTargetElement = invoker.popoverShowTargetElement; - popoverTargetElement?.showPopover(); - } else if (invoker.popoverHideTargetElement) { - popoverTargetElement = invoker.popoverHideTargetElement; - popoverTargetElement?.hidePopover(); - } - return popoverTargetElement; - }; - const onClick = (event: Event) => { const target = event.target; if (!(target instanceof Element) || target?.shadowRoot) { @@ -283,23 +230,44 @@ export function apply() { return; } const invoker = target.closest(popoverInvokerSelector); - const popoverTargetElement = handlePopoverTargetElementInvocation(invoker); - for (const popover of [...popovers]) { - if ( - popover.matches('[popover="" i].\\:open, [popover=auto i].\\:open') && - popover !== popoverTargetElement && - !queryAncestorAll(target, '[popover]').includes(popover) - ) { - popover.hidePopover(); + let popoverTargetElement: HTMLElement | null = null; + if ( + invoker instanceof HTMLButtonElement || + invoker instanceof HTMLInputElement + ) { + if (invoker.popoverToggleTargetElement) { + popoverTargetElement = invoker.popoverToggleTargetElement; + if (popoverTargetElement) { + if (visibleElements.has(popoverTargetElement)) { + popoverTargetElement.hidePopover(); + } else { + popoverTargetElement.showPopover(); + } + } + } else if (invoker.popoverShowTargetElement) { + popoverTargetElement = invoker.popoverShowTargetElement; + if ( + popoverTargetElement && + !visibleElements.has(popoverTargetElement) + ) { + popoverTargetElement.showPopover(); + } + } else if (invoker.popoverHideTargetElement) { + popoverTargetElement = invoker.popoverHideTargetElement; + if (popoverTargetElement && visibleElements.has(popoverTargetElement)) { + popoverTargetElement.hidePopover(); + } } } + hideOpenAutoPopovers( + popoverTargetElement || + closestShadowPenetrating(openPopoverSelector, target), + ); }; - const addOnClickEventListener = ( - (callback: (event: Event) => void) => (root: Document | ShadowRoot) => { - root.addEventListener('click', callback); - } - )(onClick); + const addOnClickEventListener = (root: Document | ShadowRoot) => { + root.addEventListener('click', onClick); + }; observePopoversMutations(document); addOnClickEventListener(document); diff --git a/tests/dismiss.spec.ts b/tests/dismiss.spec.ts index 604181b..fe24dc0 100644 --- a/tests/dismiss.spec.ts +++ b/tests/dismiss.spec.ts @@ -64,3 +64,20 @@ test('click inside auto popover does not dismiss itself', async ({ page }) => { await popover7.evaluate((node) => node.click()); await expect(popover7).toBeVisible(); }); + +test('showing an auto popover should close all other auto popovers', async ({ + page, +}) => { + const popover3 = (await page.locator('#popover3')).nth(0); + const popover7 = (await page.locator('#popover7')).nth(0); + await expect( + await popover3.evaluate((node) => node.showPopover()), + ).toBeUndefined(); + await expect(popover3).toBeVisible(); + await expect(popover7).toBeHidden(); + await expect( + await popover7.evaluate((node) => node.showPopover()), + ).toBeUndefined(); + await expect(popover7).toBeVisible(); + await expect(popover3).toBeHidden(); +}); From 9937623bfede5c72a5d8d56813e966e25424ae30 Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Wed, 15 Mar 2023 10:44:04 +0000 Subject: [PATCH 2/4] add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3bada9..dacdd19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- 🚀 NEW: Showing an `auto` popover closed other `auto` popovers -- + [#83](https://github.com/oddbird/popover-polyfill/pull/83) - 🚀 NEW: Add support for focus restoration on popover close -- [#81](https://github.com/oddbird/popover-polyfill/pull/81) From f368d5dd960a79ae738837fcf14da331fc1b584b Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Wed, 15 Mar 2023 16:36:57 +0000 Subject: [PATCH 3/4] Update CHANGELOG.md Co-authored-by: Jonny Gerig Meyer --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dacdd19..6b30e1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -- 🚀 NEW: Showing an `auto` popover closed other `auto` popovers -- +- 🚀 NEW: Showing an `auto` popover closes other `auto` popovers -- [#83](https://github.com/oddbird/popover-polyfill/pull/83) - 🚀 NEW: Add support for focus restoration on popover close -- [#81](https://github.com/oddbird/popover-polyfill/pull/81) From 87587e697d776e306011119b23e6837311998aa0 Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Wed, 15 Mar 2023 16:37:49 +0000 Subject: [PATCH 4/4] Remove dead code --- src/popover-helpers.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/popover-helpers.ts b/src/popover-helpers.ts index 30d21c7..f2a6451 100644 --- a/src/popover-helpers.ts +++ b/src/popover-helpers.ts @@ -55,21 +55,6 @@ export function closestShadowPenetrating( return closestShadowPenetrating(selector, root.host); } -export function hasPopoverAncestor( - element: Element, - popover: Element, -): boolean { - let parent = element; - do { - const ancestor = closestShadowPenetrating('[popover]', parent); - if (!ancestor) return false; - if (ancestor === popover) return true; - parent = - ancestor.parentElement || (ancestor.getRootNode() as ShadowRoot)?.host; - } while (parent); - return false; -} - export function setInvokerAriaExpanded( el: HTMLButtonElement | HTMLInputElement, ) {