From 3a2446eb27fde085f74c78af9df9e73d87a82c6f Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Tue, 28 Mar 2023 11:05:37 +0100 Subject: [PATCH 1/3] patch qSA/matches/closest to accept :popover-open --- README.md | 30 ++++++++++++++++++------------ src/popover-helpers.ts | 4 ++-- src/popover.css | 18 ++++++++++++++---- src/popover.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ tests/basic.spec.ts | 35 +++++++++++++++++++++++++++++++++++ 5 files changed, 111 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index fb21a41..e9c375a 100644 --- a/README.md +++ b/README.md @@ -93,18 +93,24 @@ attributes to the HTMLElement class. This polyfill is not a perfect replacement for the native behavior; there are some caveats which will need accommodations: -- Native `popover` has an `:open` and `:closed` pseudo selector state. This is - not possible to polyfill, so instead this adds the `.\:open` CSS class to any - open popover. - - - `:closed` is not implemented due to difficulty in finding popover elements - during page load. As such, you'll need to style them using `:not(.\:open)`. - - - Using native `:open` in CSS that does not support native `popover` results - in an invalid selector, and so the entire declaration is thrown away. This - is important because if you intend to style a popover using `.\:open` it - will need to be a separate declaration. For example, - `[popover]:open, [popover].\:open` will not work. +- A native `popover` has a `:popover-open` pseudo selector when in the open + state. Pseudo selectors cannot be polyfilled within CSS, and so instead the + polyfill will add the `.\:popover-open` CSS class to any open popover. In + other words a popover in the open state will have `class=":popover-open"`. In + CSS the `:` character must be escaped with a backslash. + + - The `:popover-open` selector within JavaScript methods has been polyfilled, + so both `.querySelector(':popover-open')` _and_ + `.querySelector('.\:popover-open')` will work to select the same element. + `matches` and `closest` have also been patched, so + `.matches(':popover-open')` will work the same as + `.matches('.\:popover-open')`. + + - Using native `:popover-open` in CSS that does not support native `popover` + results in an invalid selector, and so the entire declaration is thrown + away. This is important because if you intend to style a popover using + `.\:popover-open` it will need to be a separate declaration. For example, + `[popover]:popover-open, [popover].\:popover-open` will not work. - Native `popover` elements use the `:top-layer` pseudo element which gets placed above all other elements on the page, regardless of overflow or diff --git a/src/popover-helpers.ts b/src/popover-helpers.ts index 9067f0d..f581093 100644 --- a/src/popover-helpers.ts +++ b/src/popover-helpers.ts @@ -235,7 +235,7 @@ export function showPopover(element: HTMLElement) { } previouslyFocusedElements.delete(element); const originallyFocusedElement = document.activeElement as HTMLElement; - element.classList.add(':open'); + element.classList.add(':popover-open'); visibilityState.set(element, 'showing'); if (!topLayerElements.has(document)) { topLayerElements.set(document, new Set()); @@ -293,7 +293,7 @@ export function hidePopover( } topLayerElements.get(document)?.delete(element); autoPopoverList.get(document)?.delete(element); - element.classList.remove(':open'); + element.classList.remove(':popover-open'); visibilityState.set(element, 'hidden'); if (fireEvents) { queuePopoverToggleEventTask(element, 'open', 'closed'); diff --git a/src/popover.css b/src/popover.css index 814bb81..c62964b 100644 --- a/src/popover.css +++ b/src/popover.css @@ -14,12 +14,22 @@ /* stylelint-disable selector-class-pattern */ @supports not selector([popover]:open) { - [popover]:not(.\:open) { + [popover]:not(.\:popover-open, dialog[open]) { display: none; } + + [anchor].\:popover-open { + inset: auto; + } } -/* stylelint-enable selector-class-pattern */ -[popover][anchor] { - inset: auto; +@supports not selector([popover]:popover-open) { + [popover]:not(.\:popover-open, dialog[open]) { + display: none; + } + + [anchor].\:popover-open { + inset: auto; + } } +/* stylelint-enable selector-class-pattern */ diff --git a/src/popover.ts b/src/popover.ts index 90732a0..e5fcdbe 100644 --- a/src/popover.ts +++ b/src/popover.ts @@ -16,9 +16,51 @@ export function isSupported() { ); } +function patchSelectorFn( + object: Record, + name: K, + mapper: (selector: string) => string, +) { + const original = object[name] as (selectors: string) => NodeList; + Object.defineProperty(object, name, { + value(selector: string) { + return original.call(this, mapper(selector)); + }, + }); +} + +const nonEscapedPopoverSelector = /(^|[^\\]):popover-open\b/g; + export function apply() { window.ToggleEvent = window.ToggleEvent || ToggleEvent; + function rewriteSelector(selector: string) { + if (selector.includes(':popover-open')) { + selector = selector.replace( + nonEscapedPopoverSelector, + '$1.\\:popover-open', + ); + } + return selector; + } + + patchSelectorFn(Document.prototype, 'querySelector', rewriteSelector); + patchSelectorFn(Document.prototype, 'querySelectorAll', rewriteSelector); + patchSelectorFn(Element.prototype, 'querySelector', rewriteSelector); + patchSelectorFn(Element.prototype, 'querySelectorAll', rewriteSelector); + patchSelectorFn(Element.prototype, 'matches', rewriteSelector); + patchSelectorFn(Element.prototype, 'closest', rewriteSelector); + patchSelectorFn( + DocumentFragment.prototype, + 'querySelectorAll', + rewriteSelector, + ); + patchSelectorFn( + DocumentFragment.prototype, + 'querySelectorAll', + rewriteSelector, + ); + Object.defineProperties(HTMLElement.prototype, { popover: { enumerable: true, diff --git a/tests/basic.spec.ts b/tests/basic.spec.ts index 9a36770..39b87b3 100644 --- a/tests/basic.spec.ts +++ b/tests/basic.spec.ts @@ -11,10 +11,45 @@ expect.extend({ await popover.evaluate((node) => node.showPopover()), ).toBeUndefined(); await expect(popover).toBeVisible(); + await expect( + await popover.evaluate((node) => node.matches(':popover-open')), + ).toEqual(true); + await expect( + await popover.evaluate((node) => node.closest(':popover-open') === node), + ).toEqual(true); + await expect( + await popover.evaluate((node) => node.matches(':not(:popover-open)')), + ).toEqual(false); + await expect( + await popover.evaluate( + () => document.querySelectorAll(':popover-open').length, + ), + ).toEqual(1); await expect( await popover.evaluate((node) => node.hidePopover()), ).toBeUndefined(); await expect(popover).toBeHidden(); + await expect( + await popover.evaluate((node) => node.matches(':popover-open')), + ).toEqual(false); + await expect( + await popover.evaluate((node) => node.matches(':not(:popover-open)')), + ).toEqual(true); + await expect( + await popover.evaluate( + (node) => node.closest(':not(:popover-open)') === node, + ), + ).toEqual(true); + await expect( + await popover.evaluate( + () => document.querySelectorAll(':popover-open').length, + ), + ).toEqual(0); + await expect( + await popover.evaluate( + () => document.querySelectorAll('[popover]:popover-open').length, + ), + ).toEqual(0); return { pass: true }; }, }); From eb76880893ce04c852afd7f7dcdb27034a865333 Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Tue, 4 Apr 2023 10:10:02 +0100 Subject: [PATCH 2/3] changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffa1f3f..24bde61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Popover Attribute Polyfill Changelog +## 0.2.0: 2023-03-24 + +- 🚀 NEW: Support for `:popover-open` psuedo selector + [#84](https://github.com/oddbird/popover-polyfill/pull/84) + ## 0.1.1: 2023-03-24 - 🐛 BUGFIX: Fix regression when targets have nested elements -- From 9d46d1833b895b6e447693ba03736f4ff8c4675b Mon Sep 17 00:00:00 2001 From: Jonny Gerig Meyer Date: Tue, 4 Apr 2023 10:43:04 -0400 Subject: [PATCH 3/3] Add comment about :open --- CHANGELOG.md | 14 +++++++++----- src/popover.css | 3 +++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24bde61..58bfa73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ # Popover Attribute Polyfill Changelog -## 0.2.0: 2023-03-24 +## Unreleased -- 🚀 NEW: Support for `:popover-open` psuedo selector +- 🚀 NEW: Support for `:popover-open` pseudo selector, including a polyfill for + JavaScript API methods (`querySelector`, `querySelectorAll`, `matches`, and + `closest`) -- [#84](https://github.com/oddbird/popover-polyfill/pull/84) ## 0.1.1: 2023-03-24 @@ -47,7 +49,8 @@ ## 0.0.8: 2023-01-26 -- 🚀 NEW: Add support for new [`beforetoggle` event](https://whatpr.org/html/8221/popover.html#show-popover) -- +- 🚀 NEW: Add support for new [`beforetoggle` + event](https://whatpr.org/html/8221/popover.html#show-popover) -- [#68](https://github.com/oddbird/popover-polyfill/pull/68) - 🏠 INTERNAL: Upgrade dependencies @@ -61,8 +64,9 @@ ## 0.0.6: 2023-01-17 -- 🚀 NEW: Update CSS to align closer to [Chrome's user-agent CSS](https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/core/css/popover.css) -- - [#60](https://github.com/oddbird/popover-polyfill/pull/60) +- 🚀 NEW: Update CSS to align closer to [Chrome's user-agent + CSS](https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/core/css/popover.css) + -- [#60](https://github.com/oddbird/popover-polyfill/pull/60) - 🏠 INTERNAL: Upgrade dependencies ## 0.0.5: 2023-01-14 diff --git a/src/popover.css b/src/popover.css index c62964b..df2c52d 100644 --- a/src/popover.css +++ b/src/popover.css @@ -13,6 +13,9 @@ } /* stylelint-disable selector-class-pattern */ + +/* This older `:open` pseudo selector is deprecated and support will be removed +in a later release. */ @supports not selector([popover]:open) { [popover]:not(.\:popover-open, dialog[open]) { display: none;