Files
basecoat/docs/_components/toast.njk
T

155 lines
3.9 KiB
Plaintext

{% macro toast(
id='toasts',
messages=None,
main_attrs=None
) %}
{% if registerComponent('toast') %}
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('toast', () => ({
open: false,
focusedIndex: -1,
menuItems: [],
init() {
this.$nextTick(() => {
const menuElement = this.$el.querySelector('[role="menu"]');
this.menuItems = Array.from(menuElement.querySelectorAll('[role=menuitem]'));
});
},
$trigger: {
'@click'() { this.handleClick() },
'@keydown.down.prevent'() { this.open && this.nextItem() },
':aria-expanded'() { return this.open },
':aria-pressed'() { return this.open }
},
$menu: {
'@click'(e) { this.handleMenuItemClick(e) },
'@keydown.escape.window'() { 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': ''
},
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);
});
},
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;
}
},
}));
});
</script>
{% endif %}
<div
id="{{ id }}"
class="toasts"
x-data="toast"
{% for key, value in main_attrs %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% for category, message in toasts %}
<div
x-data="{
active: true,
init() {
if ('{{ category }}' !== 'error') setTimeout(() => this.active = false, 3000)
}
}"
role="{{ 'alert' if category == 'error' else 'status' }}"
aria-atomic="true"
aria-hidden="true"
:aria-hidden="!active"
:inert="!active"
>
<button
x-on:click="active = !active"
aria-label="{{ _('Dismiss') }}"
>
{% include "icons/x.svg" %}
</button>
<section class="w-full text-left flex-1">
{% if message is string %}
<p class="font-medium w-full text-left flex-1">{{ message }}</p>
{% else %}
<h4 class="font-medium mb-1">{{ message.title }}</h4>
<p class="text-muted-foreground">{{ message.description }}</p>
{% if message.action %}
<a
href="{{ message.action.url }}"
class="btn-outline h-8 px-2.5 mt-4"
>{{ message.action.label }}</a>
{% endif %}
{% endif %}
</section>
{% if category in ('error', 'success') %}
{% if category == 'error' %}
<div class="text-danger py-0.5">
{% include "icons/circle-x.svg" %}
</div>
{% elif category == 'success' %}
<div class="text-success py-0.5">
{% include "icons/circle-check.svg" %}
</div>
{% endif %}
{% endif %}
</div>
{% endfor %}
</div>
{% endmacro %}
{% macro toasts(
id=None,
messages=None,
flash=True
) %}
{% if flash %}
{% set toasts = (messages or []) + get_flashed_messages(with_categories=true) %}
{% else %}
{% set toasts = messages or [] %}
{% endif %}
<div
class="toasts"
id="{{ id or 'toasts' }}"
{}
>
</div>
{% endmacro %}