From 218c16ba6b7fd68b5d88ffdd3ded0c7fd6772205 Mon Sep 17 00:00:00 2001 From: Ronan Berder Date: Wed, 18 Jun 2025 19:42:26 +0800 Subject: [PATCH] Fixing bugs and docs issues --- docs/src/_includes/layouts/base.njk | 32 ++++--- docs/src/_includes/partials/header.njk | 4 +- .../partials/kitchen-sink/slider.njk | 2 +- docs/src/assets/styles.css | 40 ++------- docs/src/components/breadcrumb.njk | 2 +- docs/src/components/chart.njk | 2 +- docs/src/components/sidebar.njk | 6 +- docs/src/components/toast.njk | 6 +- docs/src/index.njk | 35 ++++---- docs/src/installation.njk | 14 +-- src/css/basecoat.css | 2 +- src/jinja/select.html.jinja | 34 +++---- src/js/dark-mode.js | 10 --- src/js/dropdown-menu.js | 46 ++++++---- src/js/popover.js | 13 +-- src/js/select.js | 89 ++++++++++++++----- src/js/sidebar.js | 26 +++--- src/js/tabs.js | 12 +-- src/js/toast.js | 16 ++-- 19 files changed, 211 insertions(+), 180 deletions(-) delete mode 100644 src/js/dark-mode.js diff --git a/docs/src/_includes/layouts/base.njk b/docs/src/_includes/layouts/base.njk index 23837d9..65348f9 100644 --- a/docs/src/_includes/layouts/base.njk +++ b/docs/src/_includes/layouts/base.njk @@ -3,25 +3,37 @@ {# Dark mode #} - + {# Theme variant #} diff --git a/docs/src/_includes/partials/header.njk b/docs/src/_includes/partials/header.njk index 6353274..c9884bd 100644 --- a/docs/src/_includes/partials/header.njk +++ b/docs/src/_includes/partials/header.njk @@ -1,7 +1,7 @@
+ diff --git a/docs/src/components/chart.njk b/docs/src/components/chart.njk index d73d7a6..492271c 100644 --- a/docs/src/components/chart.njk +++ b/docs/src/components/chart.njk @@ -575,7 +575,7 @@ const formatDateLabel = (dateString) => { try { const date = new Date(dateString + 'T00:00:00'); return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); // e.g., "Apr 1" - } catch (e) { + } catch (event) { return dateString; // Fallback } }; diff --git a/docs/src/components/sidebar.njk b/docs/src/components/sidebar.njk index 3aa12b7..46cdb25 100644 --- a/docs/src/components/sidebar.njk +++ b/docs/src/components/sidebar.njk @@ -32,7 +32,7 @@ toc:

You can either include the JavaScript file for all the components, or just the one for this component by adding this to the <head> of your page:

-{% set code_script %}{% endset %} +{% set code_script %}{% endset %} {{ code_block(code_script | prettyHtml, "html") }}
@@ -76,7 +76,7 @@ toc: menu=menu ) }}
- +

Content

{% endset %} @@ -95,7 +95,7 @@ toc: -

Any elememt on the page can toggle, open or close the sidebar by dispatching the sidebar:toggle, sidebar:open or sidebar:close events.

+

Any elememt on the page can toggle, open or close the sidebar by dispatching the basecoat:sidebar, sidebar:open or sidebar:close events.

You can include the JavaScript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. x-bind, x-data, @click).

diff --git a/docs/src/components/toast.njk b/docs/src/components/toast.njk index b1e6a08..2e0967a 100644 --- a/docs/src/components/toast.njk +++ b/docs/src/components/toast.njk @@ -94,6 +94,10 @@ toc: {% set code_toaster %}
{% endset %} {{ code_block(code_toaster | prettyHtml, "html") }} +
+

You can set up the alignment of the toaster using the data-align attribute: start, center, or end (default to end).

+
+

Step 3: Add your toasts

@@ -248,7 +252,7 @@ toc: {{ toast( title="Event has been created", description="Sunday, December 03, 2023 at 9:00 AM", - cancel={ label: "Undo", click: "close()" } + cancel={ label: "Undo" } ) }}{% endraw %}{% endset %} {% from "tabs.njk" import tabs %} diff --git a/docs/src/index.njk b/docs/src/index.njk index 3eb3b85..ba7a3d1 100644 --- a/docs/src/index.njk +++ b/docs/src/index.njk @@ -51,15 +51,17 @@ layout: layouts/layout.njk

p@example.com

- {% call select( + {{ select( is_combobox=true, - popover_attrs={"data-align": "end"} - ) %} -
Viewer
-
Developer
-
Billing
-
Owner
- {% endcall %} + popover_attrs={"data-align": "end"}, + items=[ + { type: "item", value: "", label: "Empty" }, + { type: "item", value: "viewer", label: "Viewer" }, + { type: "item", value: "developer", label: "Developer" }, + { type: "item", value: "billing", label: "Billing" }, + { type: "item", value: "owner", label: "Owner" } + ] + ) }}
  • Isabella Nguyen @@ -68,15 +70,16 @@ layout: layouts/layout.njk

    i@example.com

  • - {% call select( + {{ select( is_combobox=true, - popover_attrs={"data-align": "end"} - ) %} -
    Viewer
    -
    Developer
    -
    Billing
    -
    Owner
    - {% endcall %} + popover_attrs={"data-align": "end"}, + items=[ + { type: "item", value: "viewer", label: "Viewer" }, + { type: "item", value: "developer", label: "Developer" }, + { type: "item", value: "billing", label: "Billing" }, + { type: "item", value: "owner", label: "Owner" } + ] + ) }} diff --git a/docs/src/installation.njk b/docs/src/installation.njk index 7f41d30..d0bef89 100644 --- a/docs/src/installation.njk +++ b/docs/src/installation.njk @@ -171,6 +171,8 @@ import 'basecoat-css/tabs';{% endraw %}{% endset %}

    For more complex components, you can use Nunjucks or Jinja macros to help you generate the HTML and JavaScript code.

    To install them, either copy them directly from the GitHub repository or use the CLI to do the work for you.

    For example, here's how you could use the select() macro to generate the HTML and JavaScript code for a Select component:

    + + {% set code %}{% raw %}{% call select() %}
    Fruits @@ -182,16 +184,6 @@ import 'basecoat-css/tabs';{% endraw %}{% endset %}
    {% endcall %}{% endraw %}{% endset %} {{ code_block(code, "jinja") }} -

    Make sure to include the JavaScript code before Alpine.js core JS file:

    - {% set code %} - -{% endset %} -{{ code_block(code, "jinja") }} -

    Theming

    @@ -233,7 +225,7 @@ import 'basecoat-css/tabs';{% endraw %}{% endset %} {% set code %}@import "tailwindcss"; @import "basecoat-css"; -@import "theme.css";{% endset %} +@import "./custom.css";{% endset %} {{ code_block(code, "css") }}
    diff --git a/src/css/basecoat.css b/src/css/basecoat.css index e84b085..bf70312 100644 --- a/src/css/basecoat.css +++ b/src/css/basecoat.css @@ -504,7 +504,7 @@ /* Dropdown Menu */ @layer components { .dropdown-menu { - @apply relative inline-flex p-1; + @apply relative inline-flex; [data-popover] { @apply p-1; diff --git a/src/jinja/select.html.jinja b/src/jinja/select.html.jinja index 8ca5bb8..b053b6f 100644 --- a/src/jinja/select.html.jinja +++ b/src/jinja/select.html.jinja @@ -16,7 +16,7 @@ id=None, selected=None, name=None, - items=None, + items=[], main_attrs={}, trigger_attrs={}, popover_attrs={}, @@ -30,25 +30,27 @@ {% set first_option = namespace(item=None) %} {% set selected_option = namespace(item=None) %} -{% for item in items %} - {% if item.type == "group" %} - {% for sub_item in item.items %} +{% if items %} + {% for item in items %} + {% if item.type == "group" %} + {% for sub_item in item.items %} + {% if not first_option.item %} + {% set first_option.item = sub_item %} + {% endif %} + {% if selected and sub_item.value == selected and not selected_option.item %} + {% set selected_option.item = sub_item %} + {% endif %} + {% endfor %} + {% else %} {% if not first_option.item %} - {% set first_option.item = sub_item %} + {% set first_option.item = item %} {% endif %} - {% if selected and sub_item.value == selected and not selected_option.item %} - {% set selected_option.item = sub_item %} + {% if selected and item.value == selected and not selected_option.item %} + {% set selected_option.item = item %} {% endif %} - {% endfor %} - {% else %} - {% if not first_option.item %} - {% set first_option.item = item %} {% endif %} - {% if selected and item.value == selected and not selected_option.item %} - {% set selected_option.item = item %} - {% endif %} - {% endif %} -{% endfor %} + {% endfor %} +{% endif %} {% set default_option = selected_option.item or first_option.item or None %} diff --git a/src/js/dark-mode.js b/src/js/dark-mode.js deleted file mode 100644 index f386e3e..0000000 --- a/src/js/dark-mode.js +++ /dev/null @@ -1,10 +0,0 @@ -(() => { - document.body.addEventListener('click', (event) => { - const toggleButton = event.target.closest('[data-action="toggle-dark-mode"]'); - - if (!toggleButton) return; - - const isDark = document.documentElement.classList.toggle('dark'); - localStorage.setItem('darkMode', isDark ? 'dark' : 'light'); - }); -})(); \ No newline at end of file diff --git a/src/js/dropdown-menu.js b/src/js/dropdown-menu.js index 4a4dc1f..b3af44c 100644 --- a/src/js/dropdown-menu.js +++ b/src/js/dropdown-menu.js @@ -5,7 +5,11 @@ const menu = popover.querySelector('[role="menu"]'); if (!trigger || !menu || !popover) { - console.error('Dropdown menu component is missing a trigger, a menu, or a popover content element.', dropdownMenuComponent); + const missing = []; + if (!trigger) missing.push('trigger'); + if (!menu) missing.push('menu'); + if (!popover) missing.push('popover'); + console.error(`Dropdown menu initialisation failed. Missing element(s): ${missing.join(', ')}`, dropdownMenuComponent); return; } @@ -18,7 +22,7 @@ trigger.removeAttribute('aria-activedescendant'); popover.setAttribute('aria-hidden', 'true'); trigger.focus(); - activeIndex = -1; + setActiveItem(-1); }; const openMenu = () => { @@ -53,17 +57,17 @@ } }); - dropdownMenuComponent.addEventListener('keydown', (e) => { + dropdownMenuComponent.addEventListener('keydown', (event) => { const isExpanded = trigger.getAttribute('aria-expanded') === 'true'; - if (e.key === 'Escape') { + if (event.key === 'Escape') { if (isExpanded) closeMenu(); return; } if (!isExpanded) { - if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(e.key)) { - e.preventDefault(); + if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(event.key)) { + event.preventDefault(); openMenu(); } return; @@ -73,26 +77,30 @@ let nextIndex = activeIndex; - switch (e.key) { + switch (event.key) { case 'ArrowDown': - e.preventDefault(); - nextIndex = activeIndex < menuItems.length - 1 ? activeIndex + 1 : 0; - break; + event.preventDefault(); + if (activeIndex < menuItems.length - 1) { + nextIndex = activeIndex + 1; + } + break; case 'ArrowUp': - e.preventDefault(); - nextIndex = activeIndex > 0 ? activeIndex - 1 : menuItems.length - 1; + event.preventDefault(); + if (activeIndex > 0) { + nextIndex = activeIndex - 1; + } break; case 'Home': - e.preventDefault(); + event.preventDefault(); nextIndex = 0; break; case 'End': - e.preventDefault(); + event.preventDefault(); nextIndex = menuItems.length - 1; break; case 'Enter': case ' ': - e.preventDefault(); + event.preventDefault(); menuItems[activeIndex]?.click(); closeMenu(); return; @@ -103,14 +111,14 @@ } }); - menu.addEventListener('click', (e) => { - if (e.target.closest('[role^="menuitem"]')) { + menu.addEventListener('click', (event) => { + if (event.target.closest('[role^="menuitem"]')) { closeMenu(); } }); - document.addEventListener('click', (e) => { - if (!dropdownMenuComponent.contains(e.target)) { + document.addEventListener('click', (event) => { + if (!dropdownMenuComponent.contains(event.target)) { closeMenu(); } }); diff --git a/src/js/popover.js b/src/js/popover.js index e7eb695..d77543a 100644 --- a/src/js/popover.js +++ b/src/js/popover.js @@ -4,7 +4,10 @@ const content = popoverComponent.querySelector(':scope > [data-popover]'); if (!trigger || !content) { - console.error('Popover component is missing a trigger button or a content element.', popoverComponent); + const missing = []; + if (!trigger) missing.push('trigger'); + if (!content) missing.push('content'); + console.error(`Popover initialisation failed. Missing element(s): ${missing.join(', ')}`, popoverComponent); return; } @@ -36,14 +39,14 @@ } }); - popoverComponent.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { + popoverComponent.addEventListener('keydown', (event) => { + if (event.key === 'Escape') { closePopover(); } }); - document.addEventListener('click', (e) => { - if (!popoverComponent.contains(e.target)) { + document.addEventListener('click', (event) => { + if (!popoverComponent.contains(event.target)) { closePopover(); } }); diff --git a/src/js/select.js b/src/js/select.js index 1b02ff4..73f2c22 100644 --- a/src/js/select.js +++ b/src/js/select.js @@ -6,12 +6,25 @@ const listbox = popover.querySelector('[role="listbox"]'); const input = selectComponent.querySelector(':scope > input[type="hidden"]'); const filter = selectComponent.querySelector('header input[type="text"]'); - if (!trigger || !popover || !listbox || !input) return; + if (!trigger || !popover || !listbox || !input) { + const missing = []; + if (!trigger) missing.push('trigger'); + if (!popover) missing.push('popover'); + if (!listbox) missing.push('listbox'); + if (!input) missing.push('input'); + console.error(`Select component initialisation failed. Missing element(s): ${missing.join(', ')}`, selectComponent); + return; + } const options = Array.from(listbox.querySelectorAll('[role="option"]')); let visibleOptions = [...options]; let activeIndex = -1; + const hasTransition = () => { + const style = getComputedStyle(popover); + return parseFloat(style.transitionDuration) > 0 || parseFloat(style.transitionDelay) > 0; + }; + const updateValue = (option) => { if (option) { selectedValue.innerHTML = option.dataset.label || option.innerHTML; @@ -21,14 +34,26 @@ } }; - const closePopover = () => { + const closePopover = (focusOnTrigger = true) => { + if (popover.getAttribute('aria-hidden') === 'true') return; + + if (filter) { + const resetFilter = () => { + filter.value = ''; + visibleOptions = [...options]; + options.forEach(opt => opt.setAttribute('aria-hidden', 'false')); + }; + + if (hasTransition()) { + popover.addEventListener('transitionend', resetFilter, { once: true }); + } else { + resetFilter(); + } + } + + if (focusOnTrigger) trigger.focus(); popover.setAttribute('aria-hidden', 'true'); trigger.setAttribute('aria-expanded', 'false'); - if (filter) { - filter.value = ''; - visibleOptions = [...options]; - options.forEach(opt => opt.setAttribute('aria-hidden', 'false')); - } trigger.removeAttribute('aria-activedescendant'); if (activeIndex > -1) options[activeIndex]?.classList.remove('active'); activeIndex = -1; @@ -37,10 +62,17 @@ const selectOption = (option) => { if (!option) return; - if (option.dataset.value) { + if (option.dataset.value != null) { updateValue(option); } + closePopover(); + + const event = new CustomEvent('change', { + detail: { value: option.dataset.value }, + bubbles: true + }); + selectComponent.dispatchEvent(event); }; if (filter) { @@ -67,36 +99,36 @@ filter.addEventListener('input', filterOptions); } - let initialOption = options.find(opt => input.value && opt.dataset.value === input.value); + let initialOption = options.find(opt => opt.dataset.value === input.value); if (!initialOption && options.length > 0) initialOption = options[0]; updateValue(initialOption); - const handleKeyNavigation = (e) => { + const handleKeyNavigation = (event) => { const isPopoverOpen = popover.getAttribute('aria-hidden') === 'false'; - if (!['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End', 'Escape'].includes(e.key)) { + if (!['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End', 'Escape'].includes(event.key)) { return; } if (!isPopoverOpen) { - if (e.key !== 'Enter' && e.key !== 'Escape') { - e.preventDefault(); + if (event.key !== 'Enter' && event.key !== 'Escape') { + event.preventDefault(); trigger.click(); } return; } - e.preventDefault(); + event.preventDefault(); - if (e.key === 'Escape') { + if (event.key === 'Escape') { closePopover(); return; } - if (e.key === 'Enter') { + if (event.key === 'Enter') { if (activeIndex > -1) { - selectOption(visibleOptions[activeIndex]); + selectOption(options[activeIndex]); } return; } @@ -106,7 +138,7 @@ const currentVisibleIndex = activeIndex > -1 ? visibleOptions.indexOf(options[activeIndex]) : -1; let nextVisibleIndex = currentVisibleIndex; - switch (e.key) { + switch (event.key) { case 'ArrowDown': if (currentVisibleIndex < visibleOptions.length - 1) { nextVisibleIndex = currentVisibleIndex + 1; @@ -154,9 +186,18 @@ 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'); - if (filter) filter.focus(); const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]'); if (selectedOption) { @@ -173,16 +214,16 @@ } }); - listbox.addEventListener('click', (e) => { - const clickedOption = e.target.closest('[role="option"]'); + listbox.addEventListener('click', (event) => { + const clickedOption = event.target.closest('[role="option"]'); if (clickedOption) { selectOption(clickedOption); } }); - document.addEventListener('click', (e) => { - if (!selectComponent.contains(e.target)) { - closePopover(); + document.addEventListener('click', (event) => { + if (!selectComponent.contains(event.target)) { + closePopover(false); } }); diff --git a/src/js/sidebar.js b/src/js/sidebar.js index 4994b56..a7fb24c 100644 --- a/src/js/sidebar.js +++ b/src/js/sidebar.js @@ -55,18 +55,24 @@ const sidebarId = sidebarComponent.id; - window.addEventListener('sidebar:open', (e) => { - if (!e.detail?.id || e.detail.id === sidebarId) setState(true); - }); - window.addEventListener('sidebar:close', (e) => { - if (!e.detail?.id || e.detail.id === sidebarId) setState(false); - }); - window.addEventListener('sidebar:toggle', (e) => { - if (!e.detail?.id || e.detail.id === sidebarId) setState(!open); + document.addEventListener('basecoat:sidebar', (event) => { + if (event.detail?.id && event.detail.id !== sidebarId) return; + + switch (event.detail?.action) { + case 'open': + setState(true); + break; + case 'close': + setState(false); + break; + default: + setState(!open); + break; + } }); - sidebarComponent.addEventListener('click', (e) => { - const target = e.target; + sidebarComponent.addEventListener('click', (event) => { + const target = event.target; const nav = sidebarComponent.querySelector('nav'); const isMobile = window.innerWidth < breakpoint; diff --git a/src/js/tabs.js b/src/js/tabs.js index 3979797..52eccc0 100644 --- a/src/js/tabs.js +++ b/src/js/tabs.js @@ -19,19 +19,19 @@ if (activePanel) activePanel.hidden = false; }; - tablist.addEventListener('click', (e) => { - const clickedTab = e.target.closest('[role="tab"]'); + tablist.addEventListener('click', (event) => { + const clickedTab = event.target.closest('[role="tab"]'); if (clickedTab) selectTab(clickedTab); }); - tablist.addEventListener('keydown', (e) => { - const currentTab = e.target; + tablist.addEventListener('keydown', (event) => { + const currentTab = event.target; if (!tabs.includes(currentTab)) return; let nextTab; const currentIndex = tabs.indexOf(currentTab); - switch (e.key) { + switch (event.key) { case 'ArrowRight': nextTab = tabs[(currentIndex + 1) % tabs.length]; break; @@ -48,7 +48,7 @@ return; } - e.preventDefault(); + event.preventDefault(); selectTab(nextTab); nextTab.focus(); }); diff --git a/src/js/toast.js b/src/js/toast.js index 921cc50..26256ee 100644 --- a/src/js/toast.js +++ b/src/js/toast.js @@ -15,11 +15,11 @@ toaster.addEventListener('mouseenter', pauseAllTimeouts); toaster.addEventListener('mouseleave', resumeAllTimeouts); - toaster.addEventListener('click', (e) => { - const actionLink = e.target.closest('.toast footer a'); - const actionButton = e.target.closest('.toast footer button'); + toaster.addEventListener('click', (event) => { + const actionLink = event.target.closest('.toast footer a'); + const actionButton = event.target.closest('.toast footer button'); if (actionLink || actionButton) { - closeToast(e.target.closest('.toast')); + closeToast(event.target.closest('.toast')); } }); @@ -109,8 +109,8 @@ try { const func = new Function('close', actionString); func(() => closeToast(toast)); - } catch (e) { - console.error('Error executing toast action:', e); + } catch (event) { + console.error('Error executing toast action:', event); } } @@ -166,12 +166,12 @@ const initialToaster = document.getElementById('toaster'); if (initialToaster) initToaster(initialToaster); - window.addEventListener('basecoat:toast', (e) => { + window.addEventListener('basecoat:toast', (event) => { if (!toaster) { console.error('Cannot create toast: toaster container not found on page.'); return; } - const config = e.detail?.config || {}; + const config = event.detail?.config || {}; const toastElement = createToast(config); toaster.appendChild(toastElement); });