Getting ready to clean up the package and release 0.2.0

This commit is contained in:
Ronan Berder
2025-06-16 17:58:01 +08:00
parent f51a03da79
commit 17a18c1871
34 changed files with 789 additions and 707 deletions
+44 -51
View File
@@ -504,7 +504,9 @@
/* Dropdown Menu */
@layer components {
.dropdown-menu {
[popover].popover {
@apply relative inline-flex p-1;
[data-popover] {
@apply p-1;
min-width: anchor-size(width);
@@ -517,7 +519,7 @@
@apply bg-accent text-accent-foreground;
}
}
[role='menu'] [role='heading'] {
[role='menu'] [role="presentation"] {
@apply flex px-2 py-1.5 text-sm font-medium;
}
[role='separator'] {
@@ -576,85 +578,74 @@
/* Popover */
@layer components {
[popover].popover {
@apply absolute inset-auto bg-popover text-popover-foreground overflow-x-hidden overflow-y-auto p-4 rounded-md border shadow-md z-50 visible opacity-0 scale-95 w-max transition-all transition-discrete;
[data-popover] {
@apply absolute bg-popover text-popover-foreground overflow-x-hidden overflow-y-auto rounded-md border shadow-md z-50 visible opacity-100 scale-100 min-w-full w-max transition-all;
&:not([data-side]),
&[data-side='bottom'] {
@apply -translate-y-2;
}
&[data-side='top'] {
@apply translate-y-2;
}
&[data-side='left'] {
@apply translate-x-2;
}
&[data-side='right'] {
@apply -translate-x-2;
&[aria-hidden='true'] {
@apply invisible opacity-0 scale-95;
&:not([data-side]),
&[data-side='bottom'] {
@apply -translate-y-2;
}
&[data-side='top'] {
@apply translate-y-2;
}
&[data-side='left'] {
@apply translate-x-2;
}
&[data-side='right'] {
@apply -translate-x-2;
}
}
&:not([data-side]),
&[data-side='bottom'] {
@apply mt-1 top-[anchor(bottom)];
@apply mt-1 top-full;
}
&[data-side='top'] {
@apply mb-1 bottom-[anchor(top)];
@apply mb-1 bottom-full;
}
&[data-side='left'] {
@apply mr-1 right-[anchor(left)];
@apply mr-1 right-full;
}
&[data-side='right'] {
@apply ml-1 left-[anchor(right)];
@apply ml-1 left-full;
}
&:not([data-side]),
&[data-side='bottom'],
&[data-side='top'] {
&:not([data-align]),
&[data-align='start'] {
@apply left-[anchor(left)];
@apply left-0;
}
&[data-align='end'] {
@apply right-[anchor(right)];
@apply right-0;
}
&[data-align='center'] {
@apply left-[anchor(center)] -translate-x-1/2;
@apply left-1/2 -translate-x-1/2;
}
}
&[data-side='left'],
&[data-side='right'] {
&:not([data-align]),
&[data-align='start'] {
@apply top-[anchor(top)];
@apply top-0;
}
&[data-align='end'] {
@apply bottom-[anchor(bottom)];
@apply bottom-0;
}
&[data-align='center'] {
@apply top-[anchor(center)] -translate-y-1/2;
@apply top-1/2 -translate-y-1/2;
}
}
&:is(:popover-open) {
@apply opacity-100 scale-100 translate-x-0 translate-y-0;
@starting-style {
@apply opacity-0 scale-95;
&:not([data-side]),
&[data-side='bottom'] {
@apply -translate-y-2;
}
&[data-side='top'] {
@apply translate-y-2;
}
&[data-side='left'] {
@apply translate-x-2;
}
&[data-side='right'] {
@apply -translate-x-2;
}
}
}
}
.popover {
@apply relative inline-flex;
[data-popover] {
@apply p-4;
}
}
}
/* Radio */
@@ -731,8 +722,10 @@
}
}
*:not(select).select {
[popover].popover {
@apply p-1 min-w-[anchor-size(width)];
@apply relative inline-flex;
[data-popover] {
@apply p-1;
[role='option'] {
@apply aria-hidden:hidden [&_svg]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm pl-2 py-1.5 pr-7.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;
@@ -741,7 +734,7 @@
@apply bg-[image:var(--check-icon)] bg-no-repeat bg-position-[center_right_0.5rem] bg-size-[0.875rem];
}
}
[role='listbox'] [role='heading'] {
[role='listbox'] [role="presentation"] {
@apply flex text-muted-foreground px-2 py-1.5 text-xs;
}
[role='listbox'] [role='group']:not(:has([role='option']:not([aria-hidden='true']))) {
+61 -50
View File
@@ -4,53 +4,66 @@
@param id {string} [optional] - Unique identifier for the select component.
@param selected {string} [optional] - The initially selected value.
@param name {string} [optional] - The name attribute for the hidden input storing the selected value.
@param items {array} [optional] - An array of items (objects with type, label, value, attrs, and items) to render.
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button.
@param content_attrs {object} [optional] - Additional HTML attributes for the popover content div.
@param listbox_attrs {object} [optional] - Additional HTML attributes for the listbox div.
@param input_attrs {object} [optional] - Additional HTML attributes for the hidden input.
@param search_placeholder {string} [optional] [default="Search entries..."] - Placeholder text for the search input (combobox only).
@param is_combobox {boolean} [optional] [default=False] - Renders a combobox with search functionality if True.
@param is_combobox {boolean} [optional] [default=false] - Renders a combobox with search functionality if true.
#}
{% macro select(
id=None,
selected=None,
name=None,
items=None,
main_attrs={},
trigger_attrs={},
content_attrs={},
popover_attrs={},
listbox_attrs={},
input_attrs={},
search_placeholder="Search entries...",
is_combobox=False
is_combobox=false
) %}
<div
class="popover {{ main_attrs.class }}"
x-data="select('{{ name }}', '{{ selected or '' }}')"
@click.away="open = false"
{% if id %}id="{{ id }}"{% endif %}
{% for key, value in main_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
{% set id = id or ("select-" + (range(100000, 999999) | random | string)) %}
{% set first_option = [] %}
{% set selected_option = [] %}
{% for item in items.items() %}
{% if item.type == "group" %}
{% for sub_item in item.items.items() %}
{% if not first_option[0] %}
{% set first_option = (first_option.push(sub_item), first_option) %}
{% endif %}
{% if selected and sub_item.value == selected and not selected_option[0] %}
{% set selected_option = (selected_option.push(sub_item), selected_option) %}
{% endif %}
{% endfor %}
{% else %}
{% if not first_option[0] %}
{% set first_option = (first_option.push(item), first_option) %}
{% endif %}
{% if selected and item.value == selected and not selected_option[0] %}
{% set selected_option = (selected_option.push(item), selected_option) %}
{% endif %}
{% endif %}
{% endfor %}
{% set default_option = selected_option[0] or first_option[0] or None %}
<div class="select">
<button
type="button"
class="btn-outline justify-between font-normal {{ trigger_attrs.class }}"
id="{{ id }}-trigger"
popovertarget="{{ id }}"
aria-haspopup="listbox"
aria-expanded="false"
x-bind="$trigger"
{% if id %}
id="{{ id }}-trigger"
aria-controls="{{ id }}-content"
{% endif %}
class="btn-outline justify-between font-normal {{ trigger_attrs.class }}"
{% for key, value in trigger_attrs.items() %}
aria-controls="{{ id }}-listbox"
{% for key, value in trigger_attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<div x-html="selectedLabel" class="flex items-center gap-x-2"
></div>
<span class="truncate">{{ default_option.label }}</span>
{% if is_combobox %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevrons-up-down-icon lucide-chevrons-up-down text-muted-foreground opacity-50 shrink-0"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
{% else %}
@@ -58,12 +71,11 @@
{% endif %}
</button>
<div
data-popover
aria-hidden="true"
x-bind="$content"
{% if id %}id="{{ id }}-content"{% endif %}
{% for key, value in content_attrs.items() %}
{{ key }}="{{ value }}"
popover
id="{{ id }}"
class="popover {{ popover_attrs.class }}"
{% for key, value in popover_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
{% if is_combobox %}
@@ -81,35 +93,33 @@
aria-expanded="true"
aria-controls="{{ id }}-content"
aria-labelledby="{{ id }}-trigger"
x-model="query"
x-bind="$filter"
>
</header>
{% endif %}
<div
role="listbox"
id="{{ id }}-listbox"
aria-orientation="vertical"
{% for key, value in listbox_attrs.items() %}
aria-labelledby="{{ id }}-trigger"
{% for key, value in listbox_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% if items %}
{{ render_select_items(items, id ~ "-items" if id else "items") }}
{% if items.length > 0 %}
{{ render_select_items(items, default_option.value, id ~ "-items" if id else "items") }}
{% else %}
{{ caller() if caller }}
{% endif %}
</div>
</div>
{% if name is defined %}
<input
type="hidden"
name="{{ name }}"
x-model="selectedValue"
{% for key, value in input_attrs.items() %}
{% if key != 'name' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
{% endif %}
<input
type="hidden"
name="{{ name or id ~ '-value' }}"
value="{{ selected or '' }}"
{% for key, value in input_attrs.items() %}
{% if key != 'name' and key != 'value' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
</div>
{% endmacro %}
@@ -119,29 +129,30 @@
@param items {array} - The array of items to render.
@param parent_id_prefix {string} [optional] - The prefix for the item id.
#}
{% macro render_select_items(items, parent_id_prefix="items") %}
{% macro render_select_items(items, selected, parent_id_prefix="items") %}
{% for item in items.items() %}
{% set item_id = parent_id_prefix ~ "-" ~ loop.index %}
{% if item.type == "group" %}
{% set group_label_id = item.id if item.id else "group-label-" + item_id %}
<div
role="group"
aria-labelledby="{{ group_label_id }}"
{% for key, value in item.attrs.items() %}
{% for key, value in item.attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
<div role="heading" id="{{ group_label_id }}">{{ item.label }}</div>
{{ render_select_items(item.items, item_id) if item.items }}
{{ render_select_items(item.items, selected, item_id) if item.items }}
</div>
{% elif item.type == "separator" %}
<hr role="separator" />
{% elif item.type == "item" or not item.type %}
<div
id="{{ item_id }}"
role="option"
data-value="{{ item.value }}"
{% for key, value in item.attrs.items() %}
{% if selected == item.value %}aria-selected="true"{% endif %}
{% for key, value in item.attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
+60 -45
View File
@@ -1,13 +1,35 @@
(() => {
const initDropdownMenu = (dropdownMenuComponent) => {
const trigger = dropdownMenuComponent.querySelector('[popovertarget]');
const popover = dropdownMenuComponent.querySelector('[popover]');
const menu = popover?.querySelector('[role="menu"]');
if (!trigger || !popover || !menu) return;
const trigger = dropdownMenuComponent.querySelector(':scope > button');
const popover = dropdownMenuComponent.querySelector(':scope > [data-popover]');
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);
return;
}
let menuItems = [];
let activeIndex = -1;
const closeMenu = () => {
if (trigger.getAttribute('aria-expanded') === 'false') return;
trigger.setAttribute('aria-expanded', 'false');
trigger.removeAttribute('aria-activedescendant');
popover.setAttribute('aria-hidden', 'true');
trigger.focus();
activeIndex = -1;
};
const openMenu = () => {
trigger.setAttribute('aria-expanded', 'true');
popover.setAttribute('aria-hidden', 'false');
menuItems = Array.from(menu.querySelectorAll('[role^="menuitem"]:not([disabled])'));
if (menuItems.length > 0) {
setActiveItem(0);
}
};
const setActiveItem = (index) => {
if (activeIndex > -1 && menuItems[activeIndex]) {
menuItems[activeIndex].classList.remove('active');
@@ -17,36 +39,48 @@
const activeItem = menuItems[activeIndex];
activeItem.classList.add('active');
trigger.setAttribute('aria-activedescendant', activeItem.id);
activeItem.scrollIntoView({ block: 'nearest' });
} else {
trigger.removeAttribute('aria-activedescendant');
}
};
const handleKeyDown = (e) => {
if (!popover.matches(':popover-open')) {
trigger.addEventListener('click', () => {
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
closeMenu();
} else {
openMenu();
}
});
dropdownMenuComponent.addEventListener('keydown', (e) => {
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
if (e.key === 'Escape') {
if (isExpanded) closeMenu();
return;
}
if (!isExpanded) {
if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(e.key)) {
e.preventDefault();
trigger.click();
openMenu();
}
return;
}
let nextIndex = activeIndex;
if (menuItems.length === 0) return;
let nextIndex = activeIndex;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if (activeIndex < menuItems.length - 1) {
nextIndex = activeIndex + 1;
}
nextIndex = activeIndex < menuItems.length - 1 ? activeIndex + 1 : 0;
break;
case 'ArrowUp':
e.preventDefault();
if (activeIndex > 0) {
nextIndex = activeIndex - 1;
}
nextIndex = activeIndex > 0 ? activeIndex - 1 : menuItems.length - 1;
break;
case 'Home':
e.preventDefault();
@@ -60,42 +94,24 @@
case ' ':
e.preventDefault();
menuItems[activeIndex]?.click();
break;
closeMenu();
return;
}
if (nextIndex !== activeIndex) {
setActiveItem(nextIndex);
}
};
trigger.addEventListener('keydown', handleKeyDown);
popover.addEventListener('toggle', (e) => {
trigger.setAttribute('aria-expanded', e.newState === 'open');
if (e.newState === 'open') {
menuItems = Array.from(menu.querySelectorAll('[role^="menuitem"]:not([disabled])'));
menuItems.forEach((item, index) => {
if (!item.id) item.id = `${menu.id}-item-${index}`;
});
setActiveItem(0);
} else {
setActiveItem(-1);
}
});
menu.addEventListener('click', (e) => {
if (e.target.closest('[role^="menuitem"]')) {
popover.hidePopover();
closeMenu();
}
});
menu.addEventListener('mouseover', (e) => {
const item = e.target.closest('[role^="menuitem"]:not([disabled])');
if (item) {
const index = menuItems.indexOf(item);
if (index > -1 && index !== activeIndex) {
setActiveItem(index);
}
document.addEventListener('click', (e) => {
if (!dropdownMenuComponent.contains(e.target)) {
closeMenu();
}
});
@@ -107,15 +123,14 @@
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches('.dropdown-menu:not([data-dropdown-menu-initialized])')) {
initDropdownMenu(node);
}
node.querySelectorAll('.dropdown-menu:not([data-dropdown-menu-initialized])').forEach(initDropdownMenu);
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.matches('.dropdown-menu:not([data-dropdown-menu-initialized])')) {
initDropdownMenu(node);
}
node.querySelectorAll('.dropdown-menu:not([data-dropdown-menu-initialized])').forEach(initDropdownMenu);
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
+69
View File
@@ -0,0 +1,69 @@
(() => {
const initPopover = (popoverComponent) => {
const trigger = popoverComponent.querySelector(':scope > button');
const content = popoverComponent.querySelector(':scope > [data-popover]');
if (!trigger || !content) {
console.error('Popover component is missing a trigger button or a content element.', popoverComponent);
return;
}
const closePopover = () => {
if (trigger.getAttribute('aria-expanded') === 'false') return;
trigger.setAttribute('aria-expanded', 'false');
content.setAttribute('aria-hidden', 'true');
trigger.focus();
};
const openPopover = () => {
const elementToFocus = content.querySelector('[autofocus]');
if (elementToFocus) {
content.addEventListener('transitionend', () => {
elementToFocus.focus();
}, { once: true });
}
trigger.setAttribute('aria-expanded', 'true');
content.setAttribute('aria-hidden', 'false');
};
trigger.addEventListener('click', () => {
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
closePopover();
} else {
openPopover();
}
});
popoverComponent.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closePopover();
}
});
document.addEventListener('click', (e) => {
if (!popoverComponent.contains(e.target)) {
closePopover();
}
});
popoverComponent.dataset.popoverInitialized = true;
};
document.querySelectorAll('.popover:not([data-popover-initialized])').forEach(initPopover);
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.matches('.popover:not([data-popover-initialized])')) {
initPopover(node);
}
node.querySelectorAll('.popover:not([data-popover-initialized])').forEach(initPopover);
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
+59 -49
View File
@@ -1,8 +1,8 @@
(() => {
const initSelect = (selectComponent) => {
const trigger = selectComponent.querySelector(':scope > [popovertarget]');
const trigger = selectComponent.querySelector(':scope > button');
const selectedValue = trigger.querySelector(':scope > span');
const popover = selectComponent.querySelector(':scope > [popover]');
const popover = selectComponent.querySelector(':scope > [data-popover]');
const listbox = popover.querySelector('[role="listbox"]');
const input = selectComponent.querySelector(':scope > input[type="hidden"]');
const filter = selectComponent.querySelector('header input[type="text"]');
@@ -21,15 +21,26 @@
}
};
const closePopover = () => {
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;
}
const selectOption = (option) => {
if (!option) return;
updateValue(option);
trigger.removeAttribute('aria-activedescendant');
options.forEach(opt => opt.classList.remove('active'));
activeIndex = -1;
popover.hidePopover();
if (option.dataset.value) {
updateValue(option);
}
closePopover();
};
if (filter) {
@@ -62,12 +73,14 @@
updateValue(initialOption);
const handleKeyNavigation = (e) => {
if (!['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End'].includes(e.key)) {
const isPopoverOpen = popover.getAttribute('aria-hidden') === 'false';
if (!['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End', 'Escape'].includes(e.key)) {
return;
}
if (!popover.matches(':popover-open')) {
if (e.currentTarget === trigger && e.key !== 'Enter') {
if (!isPopoverOpen) {
if (e.key !== 'Enter' && e.key !== 'Escape') {
e.preventDefault();
trigger.click();
}
@@ -76,9 +89,14 @@
e.preventDefault();
if (e.key === 'Escape') {
closePopover();
return;
}
if (e.key === 'Enter') {
if (activeIndex > -1) {
selectOption(options[activeIndex]);
selectOption(visibleOptions[activeIndex]);
}
return;
}
@@ -130,6 +148,31 @@
filter.addEventListener('keydown', handleKeyNavigation);
}
trigger.addEventListener('click', () => {
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
closePopover();
} else {
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) {
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' });
}
}
});
listbox.addEventListener('click', (e) => {
const clickedOption = e.target.closest('[role="option"]');
if (clickedOption) {
@@ -137,46 +180,13 @@
}
});
popover.addEventListener('toggle', (e) => {
trigger.setAttribute('aria-expanded', e.newState === 'open');
if (e.newState === 'open') {
if (filter) filter.focus();
const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]');
let startingOption = null;
if (selectedOption && visibleOptions.includes(selectedOption)) {
startingOption = selectedOption;
} else if (visibleOptions.length > 0) {
startingOption = visibleOptions[0];
}
if (activeIndex > -1) options[activeIndex]?.classList.remove('active');
if (startingOption) {
activeIndex = options.indexOf(startingOption);
startingOption.classList.add('active');
if (startingOption.id) {
trigger.setAttribute('aria-activedescendant', startingOption.id);
}
startingOption.scrollIntoView({ block: 'nearest' });
} else {
activeIndex = -1;
}
} else if (e.newState === 'closed') {
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;
document.addEventListener('click', (e) => {
if (!selectComponent.contains(e.target)) {
closePopover();
}
});
popover.setAttribute('aria-hidden', 'true');
selectComponent.dataset.selectInitialized = true;
};
+4 -4
View File
@@ -8,10 +8,10 @@
@param footer {string} [optional] - HTML content for the dialog footer.
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button.
@param content_attrs {object} [optional] - Additional HTML attributes for the dialog content article.
@param content_header_attrs {object} [optional] - Additional HTML attributes for the dialog header.
@param content_body_attrs {object} [optional] - Additional HTML attributes for the dialog body section.
@param content_footer_attrs {object} [optional] - Additional HTML attributes for the dialog footer.
@param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog content article.
@param header_attrs {object} [optional] - Additional HTML attributes for the dialog header.
@param body_attrs {object} [optional] - Additional HTML attributes for the dialog body section.
@param footer_attrs {object} [optional] - Additional HTML attributes for the dialog footer.
@param open {boolean} [optional] [default=false] - Whether the dialog should be open initially.
@param close_button {boolean} [optional] [default=true] - Whether to include a close button.
@param close_on_overlay_click {boolean} [optional] [default=true] - Whether clicking the overlay closes the dialog.
+18 -12
View File
@@ -6,23 +6,28 @@
@param menu {array} [optional] - Array of menu items for the dropdown.
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button.
@param content_attrs {object} [optional] - Additional HTML attributes for the dropdown content div.
@param popover_attrs {object} [optional] - Additional HTML attributes for the dropdown content div.
#}
{% macro dropdown_menu(
id=None,
trigger,
items=None,
main_attrs={},
trigger_attrs={},
popover_attrs={},
menu_attrs={}
) %}
{% set id = id or ("dropdown-menu-" + (range(100000, 999999) | random | string)) %}
<div class="dropdown-menu">
<div
class="dropdown-menu {{ main_attrs.class }}"
{% for key, value in main_attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<button
type="button"
id="{{ id }}-trigger"
popovertarget="{{ id }}"
aria-haspopup="menu"
aria-controls="{{ id }}-menu"
aria-expanded="false"
@@ -33,14 +38,14 @@
{{ trigger | safe }}
</button>
<div
popover
class="popover p-1 {% if popover_attrs.class %} {{ popover_attrs.class }}{% endif %}"
id="{{ id }}"
data-popover
aria-hidden="true"
{% for key, value in popover_attrs %}
{% if key != "class" %}{{ key }}="{{ value }}"{% endif %}
{{ key }}="{{ value }}"
{% endfor %}
>
<nav
<div
role="menu"
id="{{ id }}-menu"
aria-labelledby="{{ id }}-trigger"
@@ -53,7 +58,7 @@
{% else %}
{{ caller() if caller }}
{% endif %}
</nav>
</div>
</div>
</div>
{% endmacro %}
@@ -77,7 +82,7 @@
{{ key }}="{{ value }}"
{% endfor %}
>
<div role="heading" id="{{ group_label_id }}">{{ item.label }}</div>
<div role="presentation" id="{{ group_label_id }}">{{ item.label }}</div>
{{ render_dropdown_items(item.items, item_id) if item.items }}
</div>
{% elif item.type == "separator" %}
@@ -85,6 +90,7 @@
{% elif item.type == "item" or not item.type %}
{% if item.url %}
<a
id="{{ item_id }}"
role="menuitem"
href="{{ item.url }}"
{% for key, value in item.attrs %}
@@ -94,15 +100,15 @@
{{ item.label | safe }}
</a>
{% else %}
<button
<div
id="{{ item_id }}"
role="menuitem"
type="button"
{% for key, value in item.attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ item.label | safe }}
</button>
</div>
{% endif %}
{% endif %}
{% endfor %}
+27 -19
View File
@@ -5,35 +5,43 @@
@param trigger {string} [optional] - HTML content for the element that triggers the popover.
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger element.
@param content_attrs {object} [optional] - Additional HTML attributes for the popover content div.
@param popover_attrs {object} [optional] - Additional HTML attributes for the popover content div.
#}
{% macro popover(
id=None,
trigger,
main_attrs={},
trigger_attrs={},
popover_attrs={}
) %}
{% set id = id or ("popover-" + (range(100000, 999999) | random | string)) %}
<button
type="button"
popovertarget="{{ id }}"
id="{{ id }}-trigger"
onclick="this.setAttribute('aria-expanded', this.getAttribute('aria-expanded') === 'false' ? 'true' : 'false')"
aria-expanded="false"
{% for key, value in trigger_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ trigger | safe }}
</button>
<div
popover
id="{{ id }}"
class="popover {{ popover_attrs.class }}"
{% for key, value in popover_attrs %}
class="popover {{ main_attrs.class }}"
{% for key, value in main_attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
{{ caller() if caller }}
>
<button
id="{{ id }}-trigger"
type="button"
aria-expanded="false"
aria-controls="{{ id }}"
{% for key, value in trigger_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ trigger | safe }}
</button>
<div
id="{{ id }}"
data-popover
aria-hidden="true"
{% for key, value in popover_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ caller() if caller }}
</div>
</div>
{% endmacro %}
+34 -26
View File
@@ -6,7 +6,7 @@
@param name {string} [optional] - The name attribute for the hidden input storing the selected value.
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button.
@param content_attrs {object} [optional] - Additional HTML attributes for the popover content div.
@param popover_attrs {object} [optional] - Additional HTML attributes for the popover content div.
@param listbox_attrs {object} [optional] - Additional HTML attributes for the listbox div.
@param input_attrs {object} [optional] - Additional HTML attributes for the hidden input.
@param search_placeholder {string} [optional] [default="Search entries..."] - Placeholder text for the search input (combobox only).
@@ -17,9 +17,11 @@
selected=None,
name=None,
items=None,
main_attrs={},
trigger_attrs={},
popover_attrs={},
listbox_attrs={},
input_attrs={},
search_placeholder="Search entries...",
is_combobox=false
) %}
@@ -28,34 +30,40 @@
{% set first_option = [] %}
{% set selected_option = [] %}
{% 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[0] %}
{% set first_option = (first_option.push(sub_item), first_option) %}
{% endif %}
{% if selected and sub_item.value == selected and not selected_option[0] %}
{% set selected_option = (selected_option.push(sub_item), selected_option) %}
{% endif %}
{% endfor %}
{% else %}
{% if not first_option[0] %}
{% set first_option = (first_option.push(sub_item), first_option) %}
{% set first_option = (first_option.push(item), first_option) %}
{% endif %}
{% if selected and sub_item.value == selected and not selected_option[0] %}
{% set selected_option = (selected_option.push(sub_item), selected_option) %}
{% if selected and item.value == selected and not selected_option[0] %}
{% set selected_option = (selected_option.push(item), selected_option) %}
{% endif %}
{% endfor %}
{% else %}
{% if not first_option[0] %}
{% set first_option = (first_option.push(item), first_option) %}
{% endif %}
{% if selected and item.value == selected and not selected_option[0] %}
{% set selected_option = (selected_option.push(item), selected_option) %}
{% endif %}
{% endif %}
{% endfor %}
{% endfor %}
{% endif %}
{% set default_option = selected_option[0] or first_option[0] or None %}
<div class="select">
<div
class="select {{ main_attrs.class }}"
{% for key, value in main_attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<button
type="button"
class="btn-outline justify-between font-normal {{ trigger_attrs.class }}"
id="{{ id }}-trigger"
popovertarget="{{ id }}"
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="{{ id }}-listbox"
@@ -71,11 +79,11 @@
{% endif %}
</button>
<div
popover
id="{{ id }}"
class="popover {{ popover_attrs.class }}"
data-popover
aria-hidden="true"
{% for key, value in popover_attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% if is_combobox %}
@@ -90,8 +98,8 @@
spellcheck="false"
aria-autocomplete="list"
role="combobox"
aria-expanded="true"
aria-controls="{{ id }}-content"
aria-expanded="false"
aria-controls="{{ id }}-listbox"
aria-labelledby="{{ id }}-trigger"
>
</header>
@@ -105,7 +113,7 @@
{{ key }}="{{ value }}"
{% endfor %}
>
{% if items.length > 0 %}
{% if items and items.length > 0 %}
{{ render_select_items(items, default_option.value, id ~ "-items" if id else "items") }}
{% else %}
{{ caller() if caller }}
@@ -115,7 +123,7 @@
<input
type="hidden"
name="{{ name or id ~ '-value' }}"
value="{{ selected or '' }}"
value="{{ (default_option.value if default_option) or '' }}"
{% for key, value in input_attrs %}
{% if key != 'name' and key != 'value' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
@@ -141,7 +149,7 @@
{{ key }}="{{ value }}"
{% endfor %}
>
<div role="heading" id="{{ group_label_id }}">{{ item.label }}</div>
<div role="presentation" id="{{ group_label_id }}">{{ item.label }}</div>
{{ render_select_items(item.items, selected, item_id) if item.items }}
</div>
{% elif item.type == "separator" %}
+2 -2
View File
@@ -14,9 +14,9 @@
) %}
<div
id="{{ id }}"
class="toaster"
class="toaster {{ attrs.class }}"
{% for key, value in attrs %}
{{ key }}="{{ value }}"
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
{% for item in toasts %}