-
Appearance
+
Appearance
-
Toggle sidebar
+
Toggle sidebar
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 %}
Toggle sidebar
-
Open sidebar
-
Close sidebar {% endset %}
+{% set code_trigger %}
Toggle sidebar
+
Open sidebar
+
Close sidebar {% endset %}
{{ code_block(code_trigger | prettyHtml, "html") }}
@@ -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 %}
-
Some components need some JavaScript/a> (e.g. the Tabs component). There are two ways to get it into your project:
+
Some components need some JavaScript (e.g. the Tabs component). There are two ways to get it into your project:
Using a build tool (Vite, Webpack…)
If your project is ESM-aware you can directly import the scripts. To include all components:
diff --git a/package.json b/package.json
index 578c000..813c644 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "basecoat",
- "version": "0.2.2",
+ "version": "0.2.3",
"description": "shadcn/ui for the rest of us",
"author": "hunvreus",
"type": "module",
diff --git a/packages/cli/package.json b/packages/cli/package.json
index a6871f9..b7e2b61 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "basecoat-cli",
- "version": "0.2.2",
+ "version": "0.2.3",
"description": "Add Basecoat components to your project",
"author": "hunvreus",
"license": "MIT",
diff --git a/packages/css/package.json b/packages/css/package.json
index e5652ed..4dbad87 100644
--- a/packages/css/package.json
+++ b/packages/css/package.json
@@ -1,6 +1,6 @@
{
"name": "basecoat-css",
- "version": "0.2.2",
+ "version": "0.2.3",
"description": "Tailwind CSS for Basecoat components",
"author": {
"name": "hunvreus",
diff --git a/src/css/basecoat.css b/src/css/basecoat.css
index bf70312..b3dd794 100644
--- a/src/css/basecoat.css
+++ b/src/css/basecoat.css
@@ -514,12 +514,16 @@
[role='menuitemcheckbox'],
[role='menuitemradio'] {
@apply aria-hidden:hidden [&_svg]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none [&_svg]:shrink-0 [&_svg]:size-4 aria-disabled:opacity-50 aria-disabled:pointer-events-none disabled:opacity-50 disabled:pointer-events-none w-full truncate;
- @apply focus-visible:bg-accent focus-visible:text-accent-foreground hover:bg-accent hover:text-accent-foreground;
+
+ &:not([aria-disabled='true']) {
+ @apply focus-visible:bg-accent focus-visible:text-accent-foreground hover:bg-accent hover:text-accent-foreground;
+ }
+
&.active {
@apply bg-accent text-accent-foreground;
}
}
- [role='menu'] [role="presentation"] {
+ [role='menu'] [role='heading'] {
@apply flex px-2 py-1.5 text-sm font-medium;
}
[role='separator'] {
@@ -734,7 +738,7 @@
@apply bg-[image:var(--check-icon)] bg-no-repeat bg-position-[center_right_0.5rem] bg-size-[0.875rem];
}
}
- [role='listbox'] [role="presentation"] {
+ [role='listbox'] [role='heading'] {
@apply flex text-muted-foreground px-2 py-1.5 text-xs;
}
[role='listbox'] [role='group']:not(:has([role='option']:not([aria-hidden='true']))) {
diff --git a/src/jinja/dropdown-menu.html.jinja b/src/jinja/dropdown-menu.html.jinja
index ab4a2dd..ca39d36 100644
--- a/src/jinja/dropdown-menu.html.jinja
+++ b/src/jinja/dropdown-menu.html.jinja
@@ -84,7 +84,7 @@
{% endfor %}
{% endif %}
>
-
{{ item.label }}
+
{{ item.label }}
{{ render_dropdown_items(item.items, item_id) if item.items }}
{% elif item.type == "separator" %}
diff --git a/src/jinja/select.html.jinja b/src/jinja/select.html.jinja
index b053b6f..7459800 100644
--- a/src/jinja/select.html.jinja
+++ b/src/jinja/select.html.jinja
@@ -151,7 +151,7 @@
{% endfor %}
{% endif %}
>
- {{ item.label }}
+ {{ item.label }}
{{ render_select_items(item.items, selected, item_id) if item.items }}
{% 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 }}
+
{{ 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 %}
>
-
{{ render_select_items(item.items, selected, item_id) if item.items }}