Replace tab macro with new tab web component

This commit is contained in:
Antoine Bartuccio 2025-06-15 16:11:08 +02:00
parent 2dd4fd5c71
commit c904e41ea3
Signed by: klmp200
GPG Key ID: E7245548C53F904B
5 changed files with 105 additions and 138 deletions

View File

@ -7,7 +7,6 @@ export class Tab extends HTMLElement {
static observedAttributes = ["title", "active"];
private description = "";
private inner = "";
private initialized = false;
private active = false;
attributeChangedCallback(name: string, _oldValue?: string, newValue?: string) {
@ -22,33 +21,30 @@ export class Tab extends HTMLElement {
if (name === "title") {
this.description = newValue;
}
this.render();
this.dispatchEvent(new CustomEvent("ui-tab-updated", { bubbles: true }));
}
render() {
if (!this.initialized) {
return;
}
const active = this.active ? "active" : "";
const tabContent = this.getContentHtml();
const content = html`
getButtonTemplate() {
return html`
<button
role="tab"
?aria-selected=${active}
class="tab-header clickable ${active}"
?aria-selected=${this.active}
class="tab-header clickable ${this.active ? "active" : ""}"
@click="${() => this.setActive(true)}"
>
${this.description}
</button>
`;
}
getContentTemplate() {
return html`
<section
class="tab-content"
?hidden=${!active}
class="tab-section"
?hidden=${!this.active}
>
${unsafeHTML(tabContent)}
${unsafeHTML(this.getContentHtml())}
</section>
`;
render(content, this);
}
setActive(value: boolean) {
@ -62,12 +58,10 @@ export class Tab extends HTMLElement {
connectedCallback() {
this.inner = this.innerHTML;
this.innerHTML = "";
this.initialized = true;
this.render();
}
getContentHtml() {
const content = this.getElementsByClassName("tab-content")[0];
const content = this.getElementsByClassName("tab-section")[0];
if (content !== undefined) {
return content.innerHTML;
}
@ -75,19 +69,23 @@ export class Tab extends HTMLElement {
}
setContentHtml(value: string) {
const content = this.getElementsByClassName("tab-content")[0];
const content = this.getElementsByClassName("tab-section")[0];
if (content !== undefined) {
content.innerHTML = value;
}
this.inner = value;
this.render();
}
}
@registerComponent("ui-tab-group")
export class TabGroup extends HTMLElement {
private node: HTMLDivElement;
connectedCallback() {
this.classList.add("tabs", "shadow");
this.node = document.createElement("div");
this.node.classList.add("tabs", "shadow");
this.appendChild(this.node);
this.addEventListener("ui-tab-activated", (event: CustomEvent) => {
const target = event.detail as Tab;
for (const tab of this.getElementsByTagName("ui-tab") as HTMLCollectionOf<Tab>) {
@ -96,5 +94,27 @@ export class TabGroup extends HTMLElement {
}
}
});
this.addEventListener("ui-tab-updated", () => {
this.render();
});
this.render();
}
render() {
const tabs = Array.prototype.slice.call(
this.getElementsByTagName("ui-tab"),
) as Tab[];
render(
html`
<div class="tab-headers">
${tabs.map((tab) => tab.getButtonTemplate())}
</div>
<div class="tab-content">
${tabs.map((tab) => tab.getContentTemplate())}
</div>
`,
this.node,
);
}
}

View File

@ -0,0 +1,53 @@
@import "core/static/core/colors";
ui-tab-group {
*[hidden] {
display: none;
}
.tabs {
border-radius: 5px;
.tab-headers {
display: flex;
flex-flow: row wrap;
background-color: $primary-neutral-light-color;
padding: 3px 12px 12px;
column-gap: 20px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
.tab-header {
border: none;
padding-right: 0;
padding-left: 0;
font-size: 120%;
background-color: unset;
position: relative;
&:after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
border-bottom: 4px solid darken($primary-neutral-light-color, 10%);
border-radius: 2px;
transition: all 0.2s ease-in-out;
}
&:hover:after {
border-bottom-color: darken($primary-neutral-light-color, 20%);
}
&.active:after {
border-bottom-color: $primary-dark-color;
}
}
}
section {
padding: 20px;
}
}
}

View File

@ -352,52 +352,6 @@ body {
text-align: center;
}
.tabs {
border-radius: 5px;
.tab-headers {
display: flex;
flex-flow: row wrap;
background-color: $primary-neutral-light-color;
padding: 3px 12px 12px;
column-gap: 20px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
.tab-header {
border: none;
padding-right: 0;
padding-left: 0;
font-size: 120%;
background-color: unset;
position: relative;
&:after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
border-bottom: 4px solid darken($primary-neutral-light-color, 10%);
border-radius: 2px;
transition: all 0.2s ease-in-out;
}
&:hover:after {
border-bottom-color: darken($primary-neutral-light-color, 20%);
}
&.active:after {
border-bottom-color: $primary-dark-color;
}
}
}
section {
padding: 20px;
}
}
.tool_bar {
overflow: auto;
padding: 4px;

View File

@ -245,65 +245,3 @@
<button type="button" onclick="checkbox_{{form_id}}(true);">{% trans %}Select All{% endtrans %}</button>
<button type="button" onclick="checkbox_{{form_id}}(false);">{% trans %}Unselect All{% endtrans %}</button>
{% endmacro %}
{% macro tabs(tab_list, attrs = "") %}
{# Tab component
Parameters:
tab_list: list[tuple[str, str]] The list of tabs to display.
Each element of the list is a tuple which first element
is the title of the tab and the second element its content
attrs: str Additional attributes to put on the enclosing div
Example:
A basic usage would be as follow :
{{ tabs([("title 1", "content 1"), ("title 2", "content 2")]) }}
If you want to display more complex logic, you can define macros
and use those macros in parameters :
{{ tabs([("title", my_macro())]) }}
It's also possible to get and set the currently selected tab using Alpine.
Here, the title of the currently selected tab will be displayed.
Moreover, on page load, the tab will be opened on "tab 2".
<div x-data="{current_tab: 'tab 2'}">
<p x-text="current_tab"></p>
{{ tabs([("tab 1", "Hello"), ("tab 2", "World")], "x-model=current_tab") }}
</div>
If you want to have translated tab titles, you can enclose the macro call
in a with block :
{% with title=_("title"), content=_("Content") %}
{{ tabs([(tab1, content)]) }}
{% endwith %}
#}
<div
class="tabs shadow"
x-data="{selected: '{{ tab_list[0][0] }}'}"
x-modelable="selected"
{{ attrs }}
>
<div class="tab-headers">
{% for title, _ in tab_list %}
<button
class="tab-header clickable"
:class="{active: selected === '{{ title }}'}"
@click="selected = '{{ title }}'"
>
{{ title }}
</button>
{% endfor %}
</div>
<div class="tab-content">
{% for title, content in tab_list %}
<section x-show="selected === '{{ title }}'">
{{ content }}
</section>
{% endfor %}
</div>
</div>
{% endmacro %}

View File

@ -12,6 +12,7 @@
So we give them here.
If the aforementioned bug is resolved, you can remove this. #}
{% block additional_js %}
<script type="module" src="{{ static('bundled/core/components/tabs-index.ts') }}"></script>
<script type="module" src="{{ static("bundled/core/components/ajax-select-index.ts") }}"></script>
<script
type="module"
@ -19,6 +20,7 @@
></script>
{% endblock %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static("core/components/tabs.scss") }}">
<link rel="stylesheet" href="{{ static("bundled/core/components/ajax-select-index.css") }}">
<link rel="stylesheet" href="{{ static("core/components/ajax-select.scss") }}">
<link rel="stylesheet" href="{{ static("subscription/css/subscription.scss") }}">
@ -34,12 +36,12 @@
{% block content %}
<h3>{% trans %}New subscription{% endtrans %}</h3>
<div id="subscription-form">
{% with title1=_("Existing member"), title2=_("New member") %}
{{ tabs([
(title1, form_fragment(existing_user_form, existing_user_post_url)),
(title2, form_fragment(new_user_form, new_user_post_url)),
]) }}
{% endwith %}
</div>
<ui-tab-group id="subscription-form">
<ui-tab title="{% trans %}Existing member{% endtrans %}" active>
{{ form_fragment(existing_user_form, existing_user_post_url) }}
</ui-tab>
<ui-tab title="{% trans %}New member{% endtrans %}">
{{ form_fragment(new_user_form, new_user_post_url) }}
</ui-tab>
</ui-tab-group>
{% endblock %}