Starting work on documentation

This commit is contained in:
Ronan Berder
2025-04-25 16:42:10 +08:00
parent 4f3e9c70b9
commit 5586bf69ef
71 changed files with 3167 additions and 860 deletions
+8 -8
View File
@@ -27,21 +27,21 @@
this.open = initialOpen;
this.closeOnOverlayClick = initialCloseOnOverlayClick;
},
show() {
if (!this.open) {
this.open = true;
this.$nextTick(() => this.$el.dispatchEvent(new CustomEvent('dialog:opened', { bubbles: true })));
this.$nextTick(() => {
this.$el.dispatchEvent(new CustomEvent('dialog:opened', { bubbles: true }));
setTimeout(() => this.$refs.dialog.focus(), 50);
});
}
},
hide() {
if (this.open) {
this.open = false;
this.$el.dispatchEvent(new CustomEvent('dialog:closed', { bubbles: true }));
}
},
handleControl(event) {
const detail = event.detail;
if (detail && detail.targetId === this.id) {
@@ -52,9 +52,7 @@
},
$main: {
'@dialog:control.window'(e) {
this.handleControl(e)
},
'@dialog:control.window'(e) { this.handleControl(e) },
},
$trigger: {
@@ -63,9 +61,9 @@
},
$content: {
'@keydown.escape'() { this.open && this.hide() },
':inert'() { return !this.open },
'@click'(e) { e.target === this.$el && this.closeOnOverlayClick && this.hide() },
'@keydown.escape.prevent'() { this.open && this.hide() },
'x-cloak': ''
}
}));
@@ -97,6 +95,8 @@
<div
role="dialog"
id="{{ id }}-dialog"
x-ref="dialog"
tabindex="-1"
aria-modal="true"
aria-labelledby="{{ id }}-title"
inert
+141
View File
@@ -0,0 +1,141 @@
{% macro dropdown_menu(
id=None,
trigger=None,
main_attrs=None,
trigger_attrs=None,
content_attrs=None
) %}
{% if registerComponent('dropdown-menu') %}
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('dropdownMenu', () => ({
open: false,
focusedIndex: null,
menuItems: [],
init() {
this.$nextTick(() => {
this.menuItems = Array.from(this.$el.querySelectorAll('[role=menuitem]:not([disabled])'));
});
},
focusMenuitem() {
if (this.menuItems.length === 0) return;
if (this.focusedIndex >= this.menuItems.length) {
this.focusedIndex = this.menuItems.length - 1;
} else if (this.focusedIndex < 0 || this.focusedIndex === null) {
this.focusedIndex = 0;
}
this.menuItems.forEach(item => item.blur());
this.menuItems[this.focusedIndex].focus();
},
moveMenuitemFocus(delta) {
if (this.menuItems.length === 0) return;
let wasOpen = this.open;
if (!this.open) {
this.open = true;
this.focusedIndex = 0;
} else {
this.focusedIndex = this.focusedIndex === null
? 0
: this.focusedIndex + delta;
}
if (wasOpen) {
this.focusMenuitem();
} else {
setTimeout(() => this.focusMenuitem(), 50);
}
},
handleMenuitemClick(event) {
const menuitem = event.target.closest('[role=menuitem]');
if (menuitem && menuitem.getAttribute('aria-disabled') !== 'true' && !menuitem.disabled) {
this.$nextTick(() => {
this.$refs.trigger.focus();
this.open = false;
});
}
},
handleMenuitemMousemove(event) {
const menuitem = event.target.closest('[role=menuitem]');
if (menuitem && menuitem.getAttribute('aria-disabled') !== 'true' && !menuitem.disabled) {
this.focusedIndex = this.menuItems.indexOf(menuitem);
this.focusMenuitem();
}
},
$trigger: {
'@click'() { this.open = !this.open },
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
'@keydown.down.prevent'() { this.moveMenuitemFocus(+1); },
'@keydown.up.prevent'() { this.moveMenuitemFocus(-1); },
'@keydown.home.prevent'() { this.focusMenuitem(0) },
'@keydown.end.prevent'() { this.focusMenuitem(this.menuItems.length - 1) },
'@keydown.enter.prevent'() { this.open = !this.open },
':aria-expanded'() { return this.open },
'x-ref': 'trigger'
},
$content: {
'@click'(e) { this.handleMenuitemClick(e) },
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
'@keydown.down.prevent'() { this.moveMenuitemFocus(+1); },
'@keydown.up.prevent'() { this.moveMenuitemFocus(-1); },
'@keydown.home.prevent'() { this.focusMenuitem(0) },
'@keydown.end.prevent'() { this.focusMenuitem(this.menuItems.length - 1) },
'@mousemove'(e) { this.handleMenuitemMousemove(e) },
':aria-hidden'() { return !this.open },
'x-cloak': ''
},
}));
});
</script>
{% endif %}
<div
class="popover {% if main_attrs %}{{ main_attrs.pop('class', '') }}{% endif %}"
x-data="dropdownMenu"
@click.away="open = false"
{% if id %}id="{{ id }}"{% endif %}
{% for key, value in main_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% if trigger %}
<button
type="button"
aria-haspopup="menu"
aria-expanded="false"
x-bind="$trigger"
{% if id %}
id="{{ id }}-trigger"
aria-controls="{{ id }}-menu"
{% endif %}
{% for key, value in trigger_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ trigger | safe }}
</button>
{% endif %}
<div
data-popover
aria-hidden="true"
x-bind="$content"
{% if id %}id="{{ id }}-menu"{% endif %}
{% for key, value in content_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
<nav role="menu">
{% if caller %}{{ caller() }}{% endif %}
</nav>
</div>
</div>
{% endmacro %}
+11 -65
View File
@@ -10,76 +10,22 @@
document.addEventListener('alpine:init', () => {
Alpine.data('popover', () => ({
open: false,
focusedIndex: null,
menuItems: [],
init() {
this.$nextTick(() => {
this.menuItems = Array.from(this.$el.querySelectorAll('[role=menuitem]'));
});
},
handleTriggerClick() {
this.open = !this.open;
},
handleEscape() {
if (this.open) this.open = false;
},
focusNavItem() {
if (this.menuItems.length === 0) return;
if (this.focusedIndex >= this.menuItems.length) {
this.focusedIndex = this.menuItems.length - 1;
}
if (this.focusedIndex < 0) {
this.focusedIndex = 0;
}
this.menuItems[this.focusedIndex].focus();
},
nextNavItem() {
if (this.menuItems.length === 0) return;
if (this.focusedIndex === null) {
this.focusedIndex = 0;
} else {
this.focusedIndex++;
}
this.focusNavItem();
},
prevNavItem() {
if (this.menuItems.length === 0) return;
if (this.focusedIndex === null) {
this.focusedIndex = 0;
} else {
this.focusedIndex--;
}
this.focusNavItem();
},
handleNavItemClick(event) {
if (this.menuItems.length === 0) return;
const menuItem = event.target.closest('[role=menuitem]');
if (menuItem && !menuItem.disabled) {
menuItem.blur();
this.open = false;
}
},
$trigger: {
'@click'() { this.handleTriggerClick() },
'@keydown.down.prevent'() { this.open && this.nextNavItem() },
'@click'() { this.open = !this.open },
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
':aria-expanded'() { return this.open },
'x-ref': 'trigger'
},
$content: {
'@click'(e) { this.handleNavItemClick(e) },
'@keydown.escape.prevent'() { this.handleEscape() },
'@keydown.down.prevent'() { this.nextNavItem() },
'@keydown.up.prevent'() { this.prevNavItem() },
'@keydown.home.prevent'() { this.focusNavItem(0) },
'@keydown.end.prevent'() { this.focusNavItem(this.menuItems.length - 1) },
'@click'(e) { this.handleMenuitemClick(e) },
'@keydown.escape.prevent'() {
this.open = false;
this.$refs.trigger.focus();
},
':aria-hidden'() { return !this.open },
'x-cloak': ''
},
+12 -6
View File
@@ -46,7 +46,9 @@
this.focusedIndex = 0;
}
this.options.forEach(opt => opt.removeAttribute('data-focus'));
this.options[this.focusedIndex].setAttribute('data-focus', '');
const option = this.options[this.focusedIndex];
option.setAttribute('data-focus', '');
option.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
},
focusOnSelectedOption() {
if (this.options.length === 0 || this.selectedValue === null) return;
@@ -77,7 +79,6 @@
}
},
handleOptionMousemove(event) {
console.log("here");
const option = event.target.closest('[role=option]');
if (option && option.getAttribute('aria-disabled') !== 'true') {
this.focusedIndex = this.options.indexOf(option);
@@ -126,18 +127,19 @@
'@keydown.up.prevent'() { this.moveOptionFocus(-1); },
'@keydown.home.prevent'() { this.focusOption(0) },
'@keydown.end.prevent'() { this.focusOption(this.options.length - 1) },
'@keydown.enter.prevent'() { this.handleOptionEnter() },
'@keydown.enter.prevent'() {
if (this.open) this.handleOptionEnter();
else this.open = true;
},
':aria-expanded'() { return this.open },
'x-ref': 'trigger'
},
$content: {
'@click'(e) { this.handleOptionClick(e) },
'@mousemove'(e) { this.handleOptionMousemove(e) },
':aria-hidden'() { return !this.open },
'x-cloak': ''
},
$filter: {
'@input'(e) { this.filterOptions(e.target.value) },
'@keydown.escape.prevent'() {
@@ -180,7 +182,11 @@
>
<div x-html="selectedLabel" class="flex items-center gap-x-2"
></div>
{% lucide "chevron-down", class="text-muted-foreground opacity-50 shrink-0" %}
{% if is_combobox %}
{% lucide "chevrons-up-down", class="text-muted-foreground opacity-50 shrink-0" %}
{% else %}
{% lucide "chevron-down", class="text-muted-foreground opacity-50 shrink-0" %}
{% endif %}
</button>
<div
data-popover
+97 -90
View File
@@ -1,75 +1,73 @@
{% macro tabs(
tabs,
id,
tabsets=[],
main_attrs=None,
trigger_attrs=None,
content_attrs=None
tablist_attrs=None,
default_tab_index=1
)
%}
{% if registerComponent('toast') %}
{% if registerComponent('tabs') %}
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('toast', () => ({
open: false,
focusedIndex: -1,
menuItems: [],
document.addEventListener('alpine:init', (initialTabIndex = 0) => {
Alpine.data('tabs', () => ({
activeTabIndex: initialTabIndex,
tabs: [],
panels: [],
init() {
console.log("initialTabIndex", initialTabIndex);
this.$nextTick(() => {
const menuElement = this.$el.querySelector('[role="menu"]');
this.menuItems = Array.from(menuElement.querySelectorAll('[role=menuitem]'));
this.tabs = Array.from(this.$el.querySelectorAll('[role=tab]:not([disabled])'));
this.panels = Array.from(this.$el.querySelectorAll('[role=tabpanel]'));
if (this.tabs.length > 0) {
this.selectTab(this.tabs[initialTabIndex], false);
}
});
},
$trigger: {
'@click'() { this.handleClick() },
'@keydown.down.prevent'() { this.open && this.nextItem() },
':aria-expanded'() { return this.open },
':aria-pressed'() { return this.open }
nextTab() {
if (this.tabs.length === 0) return;
let newIndex = (this.activeTabIndex + 1) % this.tabs.length;
this.selectTab(this.tabs[newIndex]);
},
$menu: {
'@click'(e) { this.handleMenuItemClick(e) },
'@keydown.escape.prevent'() { this.handleEscape() },
'@keydown.down.prevent'() { this.nextItem() },
'@keydown.up.prevent'() { this.prevItem() },
'@keydown.home.prevent'() { this.focusItem(0) },
'@keydown.end.prevent'() { this.focusItem(this.menuItems.length - 1) },
':aria-hidden'() { return !this.open },
'x-cloak': ''
prevTab() {
if (this.tabs.length === 0) return;
let newIndex = (this.activeTabIndex - 1 + this.tabs.length) % this.tabs.length;
this.selectTab(this.tabs[newIndex]);
},
focusItem(index) {
if (index >= 0 && index < this.menuItems.length) {
this.focusedIndex = index;
this.menuItems[index].focus();
}
},
nextItem() {
this.focusItem((this.focusedIndex + 1) % this.menuItems.length);
},
prevItem() {
this.focusItem(this.focusedIndex <= 0 ? this.menuItems.length - 1 : this.focusedIndex - 1);
},
handleClick() {
this.open = !this.open;
this.$nextTick(() => {
if(this.open && this.menuItems.length) this.focusItem(0);
selectTab(tab, focus = true) {
if (!tab || this.tabs.length === 0) return;
this.tabs.forEach((t, index) => {
const isSelected = t === tab;
t.setAttribute('aria-selected', isSelected);
t.setAttribute('tabindex', isSelected ? '0' : '-1');
if (isSelected) {
this.activeTabIndex = index;
this.activeTab = t;
if (focus) {
t.focus();
}
}
});
const panelId = tab.getAttribute('aria-controls');
if (!panelId) return;
this.panels.forEach(panel => {
panel.hidden = (panel.getAttribute('id') !== panelId);
});
},
handleEscape() {
if (this.open) this.open = false;
},
handleMenuItemClick(event) {
const menuItem = event.target.closest('[role=menuitem]');
if (menuItem && !menuItem.disabled) {
menuItem.blur();
this.open = false;
}
$tablist: {
['@click'](event) {
const clickedTab = event.target.closest('[role=tab]');
if (clickedTab) {
this.selectTab(clickedTab);
}
},
['@keydown.arrow-right.prevent']() { this.nextTab() },
['@keydown.arrow-left.prevent']() { this.prevTab() },
},
}));
});
@@ -77,44 +75,53 @@
{% endif %}
<div
class="toast {% if main_attrs %}{{ main_attrs.pop('class', '') }}{% endif %}"
x-data="toast"
@click.away="open = false"
class="tabs {{ main_attrs.class }}"
x-data="tabs({{ default_tab_index }})"
{% if id %}id="{{ id }}"{% endif %}
{% for key, value in main_attrs %}
{{ key }}="{{ value }}"
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
{% if trigger %}
<button
type="button"
aria-haspopup="menu"
aria-expanded="false"
aria-pressed="false"
x-bind="$trigger"
{% if id %}
id="{{ id }}-trigger"
aria-controls="{{ id }}-menu"
{% endif %}
{% for key, value in trigger_attrs %}
<div
role="tablist"
aria-orientation="horizontal"
x-bind="$tablist"
{% for key, value in tablist_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ trigger | safe }}
</button>
{% endif %}
<nav
role="menu"
aria-orientation="vertical"
aria-hidden="true"
x-bind="$menu"
{% if id %}id="{{ id }}-menu"{% endif %}
{% for key, value in menu_attrs %}
{{ key }}="{{ value }}"
{% for tabset in tabsets %}
<button
role="tab"
id="{{ tabset.id }}-tab-{{ loop.index }}"
aria-controls="{{ tabset.id }}-panel-{{ loop.index }}"
aria-selected="{{ 'true' if loop.index == default_tab_index else 'false' }}"
tabindex="0"
{% for key, value in tabset.tab_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ tabset.tab | safe }}
</button>
{% endfor %}
>
{% if caller %}{{ caller() }}{% endif %}
</nav>
</div>
{% endmacro %}
</div>
{% for tabset in tabsets %}
{% if tabset.panel %}
<div
role="tabpanel"
id="{{ tabset.id }}-panel-{{ loop.index }}"
aria-labelledby="{{ tabset.id }}-tab-{{ loop.index }}"
tabindex="-1"
aria-selected="{{ 'true' if loop.index == default_tab_index else 'false' }}"
{% if loop.index != default_tab_index %}hidden{% endif %}
{% for key, value in tabset.panel_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{{ tabset.panel | safe }}
</div>
{% endif %}
{% endfor %}
</div>
{% endmacro %}