From 9b59f43a67008167820e47fa1cc7cf813586e913 Mon Sep 17 00:00:00 2001 From: Ronan Berder Date: Wed, 18 Jun 2025 20:55:32 +0800 Subject: [PATCH] Fixing more issues with popovers --- .../partials/kitchen-sink/alert-dialog.njk | 30 ++++----- .../partials/kitchen-sink/dropdown-menu.njk | 10 +-- docs/src/assets/styles.css | 33 +++++---- docs/src/components/combobox.njk | 2 +- docs/src/components/dropdown-menu.njk | 10 +-- docs/src/components/sidebar.njk | 12 ++-- docs/src/components/toast.njk | 2 +- docs/src/installation.njk | 2 +- package.json | 2 +- packages/cli/package.json | 2 +- packages/css/package.json | 2 +- src/css/basecoat.css | 10 ++- src/jinja/dropdown-menu.html.jinja | 2 +- src/jinja/select.html.jinja | 2 +- src/js/dropdown-menu.js | 49 +++++++++----- src/js/popover.js | 16 ++++- src/js/select.js | 67 +++++++++++-------- src/js/toast.js | 2 +- src/nunjucks/dropdown-menu.njk | 2 +- src/nunjucks/select.njk | 2 +- 20 files changed, 148 insertions(+), 111 deletions(-) diff --git a/docs/src/_includes/partials/kitchen-sink/alert-dialog.njk b/docs/src/_includes/partials/kitchen-sink/alert-dialog.njk index b4be00c..57f958b 100644 --- a/docs/src/_includes/partials/kitchen-sink/alert-dialog.njk +++ b/docs/src/_includes/partials/kitchen-sink/alert-dialog.njk @@ -8,21 +8,19 @@
-
- {% set footer %} - - - {% endset %} - {{ dialog( - id="alert-dialog-demo", - title="Are you absolutely sure?", - description="This action cannot be undone. This will permanently delete your account and remove your data from our servers.", - trigger="Open dialog", - trigger_attrs={"class": "btn-outline"}, - footer=footer, - close_button=false, - close_on_overlay_click=false - ) }} -
+ {% set footer %} + + + {% endset %} + {{ dialog( + id="alert-dialog-demo", + title="Are you absolutely sure?", + description="This action cannot be undone. This will permanently delete your account and remove your data from our servers.", + trigger="Open dialog", + trigger_attrs={"class": "btn-outline"}, + footer=footer, + close_button=false, + close_on_overlay_click=false + ) }}
\ No newline at end of file diff --git a/docs/src/_includes/partials/kitchen-sink/dropdown-menu.njk b/docs/src/_includes/partials/kitchen-sink/dropdown-menu.njk index ff79594..920cfb3 100644 --- a/docs/src/_includes/partials/kitchen-sink/dropdown-menu.njk +++ b/docs/src/_includes/partials/kitchen-sink/dropdown-menu.njk @@ -16,7 +16,7 @@ popover_attrs={"class": "min-w-56"} ) %}
- +
My Account
Profile ⇧⌘P @@ -37,7 +37,7 @@
GitHub
Support
-
API
+
API

Logout @@ -52,7 +52,7 @@ popover_attrs={"class": "min-w-56"} ) %}
- +
Account Options
{% lucide "user" %} Profile @@ -71,7 +71,7 @@

- +
Appearance
{% for group in options_timezones %}
- +
{{ group.label }}
{% for timezone in group.timezones %}
{{ timezone.label }} diff --git a/docs/src/components/dropdown-menu.njk b/docs/src/components/dropdown-menu.njk index b1c4a97..a69f0b6 100644 --- a/docs/src/components/dropdown-menu.njk +++ b/docs/src/components/dropdown-menu.njk @@ -38,7 +38,7 @@ toc: popover_attrs={"class": "min-w-56"} ) %}
- +
My Account
Profile ⇧⌘P @@ -63,7 +63,7 @@ toc:
Support
-
+
API

@@ -183,7 +183,7 @@ toc: popover_attrs={"class": "min-w-56"} ) %}
- +
My Account
Profile ⇧⌘P @@ -230,7 +230,7 @@ toc: popover_attrs={"class": "min-w-56"} ) %}
- +
Account Options
{% lucide "user" %} Profile @@ -249,7 +249,7 @@ toc:

- +
Appearance
- +

Content

{% endset %} @@ -86,9 +86,9 @@ toc:

Any element on the page can toggle, open or close the sidebar by dispatching the basecoat:sidebar. By default, the event will toggle the sidebar, but can be used to open or close if you add an action to the detail:

-{% set code_trigger %} - -{% endset %} +{% set code_trigger %} + +{% endset %} {{ code_block(code_trigger | prettyHtml, "html") }}

HTML structure

@@ -155,8 +155,8 @@ toc:
<main>
A wrapper for the content of the page.
-
<button type="button" onclick="window.dispatchEvent(new CustomEvent('sidebar:toggle'))">
-
A button to toggle the sidebar. If you want to use multiple sidebars you will need to add unique ids to the sidebars (i.e. <aside class="sidebar" id="{SIDEBAR_ID}">) and refer to them in the event detail (i.e. window.dispatchEvent(new CustomEvent('sidebar:toggle', { detail: { id: '{SIDEBAR_ID}' } }));).
+
<button type="button" onclick="document.dispatchEvent(new CustomEvent('sidebar:toggle'))">
+
A button to toggle the sidebar. If you want to use multiple sidebars you will need to add unique ids to the sidebars (i.e. <aside class="sidebar" id="{SIDEBAR_ID}">) and refer to them in the event detail (i.e. document.dispatchEvent(new CustomEvent('sidebar:toggle', { detail: { id: '{SIDEBAR_ID}' } }));).
diff --git a/docs/src/components/toast.njk b/docs/src/components/toast.njk index 39234e4..355f4e6 100644 --- a/docs/src/components/toast.njk +++ b/docs/src/components/toast.njk @@ -40,7 +40,7 @@ toc: {% set code_client_side %}
{% elif item.type == "separator" %} diff --git a/src/js/dropdown-menu.js b/src/js/dropdown-menu.js index b3af44c..af5d784 100644 --- a/src/js/dropdown-menu.js +++ b/src/js/dropdown-menu.js @@ -16,19 +16,30 @@ let menuItems = []; let activeIndex = -1; - const closeMenu = () => { + const closePopover = (focusOnTrigger = true) => { if (trigger.getAttribute('aria-expanded') === 'false') return; trigger.setAttribute('aria-expanded', 'false'); trigger.removeAttribute('aria-activedescendant'); popover.setAttribute('aria-hidden', 'true'); - trigger.focus(); + + if (focusOnTrigger) { + trigger.focus(); + } + setActiveItem(-1); }; - const openMenu = () => { + const openPopover = () => { + document.dispatchEvent(new CustomEvent('basecoat:popover', { + detail: { source: dropdownMenuComponent } + })); + trigger.setAttribute('aria-expanded', 'true'); popover.setAttribute('aria-hidden', 'false'); - menuItems = Array.from(menu.querySelectorAll('[role^="menuitem"]:not([disabled])')); + menuItems = Array.from(menu.querySelectorAll('[role^="menuitem"]')).filter(item => + !item.hasAttribute('disabled') && + item.getAttribute('aria-disabled') !== 'true' + ); if (menuItems.length > 0) { setActiveItem(0); } @@ -51,9 +62,9 @@ trigger.addEventListener('click', () => { const isExpanded = trigger.getAttribute('aria-expanded') === 'true'; if (isExpanded) { - closeMenu(); + closePopover(); } else { - openMenu(); + openPopover(); } }); @@ -61,14 +72,14 @@ const isExpanded = trigger.getAttribute('aria-expanded') === 'true'; if (event.key === 'Escape') { - if (isExpanded) closeMenu(); + if (isExpanded) closePopover(); return; } if (!isExpanded) { if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(event.key)) { event.preventDefault(); - openMenu(); + openPopover(); } return; } @@ -80,15 +91,11 @@ switch (event.key) { case 'ArrowDown': event.preventDefault(); - if (activeIndex < menuItems.length - 1) { - nextIndex = activeIndex + 1; - } - break; + nextIndex = Math.min(activeIndex + 1, menuItems.length - 1); + break; case 'ArrowUp': event.preventDefault(); - if (activeIndex > 0) { - nextIndex = activeIndex - 1; - } + nextIndex = Math.max(activeIndex - 1, 0); break; case 'Home': event.preventDefault(); @@ -102,7 +109,7 @@ case ' ': event.preventDefault(); menuItems[activeIndex]?.click(); - closeMenu(); + closePopover(); return; } @@ -113,13 +120,19 @@ menu.addEventListener('click', (event) => { if (event.target.closest('[role^="menuitem"]')) { - closeMenu(); + closePopover(); } }); document.addEventListener('click', (event) => { if (!dropdownMenuComponent.contains(event.target)) { - closeMenu(); + closePopover(); + } + }); + + document.addEventListener('basecoat:popover', (event) => { + if (event.detail.source !== dropdownMenuComponent) { + closePopover(false); } }); diff --git a/src/js/popover.js b/src/js/popover.js index d77543a..73f9740 100644 --- a/src/js/popover.js +++ b/src/js/popover.js @@ -11,14 +11,20 @@ return; } - const closePopover = () => { + const closePopover = (focusOnTrigger = true) => { if (trigger.getAttribute('aria-expanded') === 'false') return; trigger.setAttribute('aria-expanded', 'false'); content.setAttribute('aria-hidden', 'true'); - trigger.focus(); + if (focusOnTrigger) { + trigger.focus(); + } }; const openPopover = () => { + document.dispatchEvent(new CustomEvent('basecoat:popover', { + detail: { source: popoverComponent } + })); + const elementToFocus = content.querySelector('[autofocus]'); if (elementToFocus) { content.addEventListener('transitionend', () => { @@ -51,6 +57,12 @@ } }); + document.addEventListener('basecoat:popover', (event) => { + if (event.detail.source !== popoverComponent) { + closePopover(false); + } + }); + popoverComponent.dataset.popoverInitialized = true; }; diff --git a/src/js/select.js b/src/js/select.js index 73f2c22..aeb9907 100644 --- a/src/js/select.js +++ b/src/js/select.js @@ -148,7 +148,7 @@ if (currentVisibleIndex > 0) { nextVisibleIndex = currentVisibleIndex - 1; } else if (currentVisibleIndex === -1) { - nextVisibleIndex = 0; // Start from top if nothing is active + nextVisibleIndex = 0; } break; case 'Home': @@ -180,37 +180,44 @@ filter.addEventListener('keydown', handleKeyNavigation); } + const openPopover = () => { + document.dispatchEvent(new CustomEvent('basecoat:popover', { + detail: { source: selectComponent } + })); + + if (filter) { + if (hasTransition()) { + popover.addEventListener('transitionend', () => { + filter.focus(); + }, { once: true }); + } else { + filter.focus(); + } + } + + popover.setAttribute('aria-hidden', 'false'); + trigger.setAttribute('aria-expanded', 'true'); + + const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]'); + if (selectedOption) { + if (activeIndex > -1) { + options[activeIndex]?.classList.remove('active'); + } + activeIndex = options.indexOf(selectedOption); + selectedOption.classList.add('active'); + if (selectedOption.id) { + trigger.setAttribute('aria-activedescendant', selectedOption.id); + } + selectedOption.scrollIntoView({ block: 'nearest' }); + } + }; + trigger.addEventListener('click', () => { const isExpanded = trigger.getAttribute('aria-expanded') === 'true'; - if (isExpanded) { closePopover(); } else { - if (filter) { - if (hasTransition()) { - popover.addEventListener('transitionend', () => { - filter.focus(); - }, { once: true }); - } else { - filter.focus(); - } - } - - popover.setAttribute('aria-hidden', 'false'); - trigger.setAttribute('aria-expanded', 'true'); - - const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]'); - if (selectedOption) { - if (activeIndex > -1) { - options[activeIndex]?.classList.remove('active'); - } - activeIndex = options.indexOf(selectedOption); - selectedOption.classList.add('active'); - if (selectedOption.id) { - trigger.setAttribute('aria-activedescendant', selectedOption.id); - } - selectedOption.scrollIntoView({ block: 'nearest' }); - } + openPopover(); } }); @@ -227,6 +234,12 @@ } }); + document.addEventListener('basecoat:popover', (event) => { + if (event.detail.source !== selectComponent) { + closePopover(false); + } + }); + popover.setAttribute('aria-hidden', 'true'); selectComponent.dataset.selectInitialized = true; }; diff --git a/src/js/toast.js b/src/js/toast.js index 26256ee..bb26612 100644 --- a/src/js/toast.js +++ b/src/js/toast.js @@ -166,7 +166,7 @@ const initialToaster = document.getElementById('toaster'); if (initialToaster) initToaster(initialToaster); - window.addEventListener('basecoat:toast', (event) => { + document.addEventListener('basecoat:toast', (event) => { if (!toaster) { console.error('Cannot create toast: toaster container not found on page.'); return; diff --git a/src/nunjucks/dropdown-menu.njk b/src/nunjucks/dropdown-menu.njk index 5d608b7..b351463 100644 --- a/src/nunjucks/dropdown-menu.njk +++ b/src/nunjucks/dropdown-menu.njk @@ -84,7 +84,7 @@ {% endfor %} {% endif %} > - +
{{ item.label }}
{{ render_dropdown_items(item.items, item_id) if item.items }}
{% elif item.type == "separator" %} diff --git a/src/nunjucks/select.njk b/src/nunjucks/select.njk index 60ba1d1..6051e3c 100644 --- a/src/nunjucks/select.njk +++ b/src/nunjucks/select.njk @@ -151,7 +151,7 @@ {% endfor %} {% endif %} > - +
{{ item.label }}
{{ render_select_items(item.items, selected, item_id) if item.items }}
{% elif item.type == "separator" %}