feat: extract collapsible container into partial

This commit is contained in:
Björn Fromme
2026-08-17 16:07:26 +02:00
parent a5d97f1206
commit f00351706e
4 changed files with 69 additions and 50 deletions
+5 -1
View File
@@ -2,7 +2,7 @@ import {Controller} from '@hotwired/stimulus'
export default class extends Controller { export default class extends Controller {
static classes = ['closed', 'open', 'iconOpen'] static classes = ['closed', 'open', 'iconOpen']
static targets = ['content', 'icon', 'container'] static targets = ['content', 'icon', 'container', 'trigger']
static values = { static values = {
open: { open: {
type: Boolean, type: Boolean,
@@ -43,6 +43,10 @@ export default class extends Controller {
this.iconTarget.classList.toggle(iconClass, true === open) this.iconTarget.classList.toggle(iconClass, true === open)
} }
if (this.hasTriggerTarget) {
this.triggerTarget.setAttribute('aria-expanded', String(open))
}
// Toggle class on container element (or controller element) if specified // Toggle class on container element (or controller element) if specified
if (this.hasOpenClass) { if (this.hasOpenClass) {
const container = this.hasContainerTarget ? this.containerTarget : this.element const container = this.hasContainerTarget ? this.containerTarget : this.element
+1
View File
@@ -21,6 +21,7 @@
<symbol id="icon-download" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" /></symbol> <symbol id="icon-download" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" /></symbol>
<symbol id="icon-sort" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" /></symbol> <symbol id="icon-sort" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" /></symbol>
<symbol id="icon-arrow-left" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75" /></symbol> <symbol id="icon-arrow-left" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75" /></symbol>
<symbol id="icon-chevron-down" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" /></symbol>
<symbol id="icon-arrow-right" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" /></symbol> <symbol id="icon-arrow-right" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" /></symbol>
<symbol id="icon-document" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /></symbol> <symbol id="icon-document" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /></symbol>
<symbol id="icon-filter" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z" /></symbol> <symbol id="icon-filter" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z" /></symbol>

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

@@ -0,0 +1,35 @@
{#
Collapsible section with a heading trigger and a chevron.
heading string visible heading text (required)
id string unique per page; wires aria-controls (required)
open bool initial state (default false)
headingLevel int aria-level of the heading (default 2)
contentClass string classes on the body wrapper (default 'rte-content')
storageKey string persist open state in sessionStorage (default '')
#}
<div {{ stimulus_controller('toggle',
{ 'open': open|default(false), 'storageKey': storageKey|default('') },
{ 'closed': 'hidden', 'iconOpen': 'rotate-180' }) }}>
<button type="button"
class="flex items-center justify-between w-full text-left"
data-action="toggle#toggle"
data-toggle-target="trigger"
aria-expanded="{{ open|default(false) ? 'true' : 'false' }}"
aria-controls="{{ id }}-content">
<span class="block text-2xl md:text-3xl font-bold mb-2"
role="heading"
aria-level="{{ headingLevel|default(2) }}">
{{ heading }}
</span>
<span class="shrink-0 transition-transform" data-toggle-target="icon">
{{ icon('chevron-down', 'w-5 h-5') }}
</span>
</button>
<div id="{{ id }}-content"
class="{{ contentClass|default('rte-content') }}"
data-toggle-target="content">
{% block body %}{% endblock %}
</div>
</div>
+28 -49
View File
@@ -97,25 +97,11 @@
{# hotel description #} {# hotel description #}
{% if ctx.hotelCmsData is not null %} {% if ctx.hotelCmsData is not null %}
<div {{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden', 'iconOpen': 'rotate-180' }) }}> {% embed '_partials/_collapsible.html.twig' with {
<button type="button" heading: 'Die Unterkunft',
class="flex items-center justify-between w-full text-left" id: 'accommodation',
data-action="toggle#toggle"> } %}
<span class="block text-2xl md:text-3xl font-bold mb-2" {% block body %}
role="heading"
aria-level="2">
Die Unterkunft
</span>
<svg class="w-5 h-5 shrink-0 transition-transform"
data-toggle-target="icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256">
<rect width="256" height="256" fill="none"></rect>
<polyline points="208 96 128 176 48 96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></polyline>
</svg>
</button>
<div class="rte-content" data-toggle-target="content">
{% set icons = ctx.hotelCmsData.icons %} {% set icons = ctx.hotelCmsData.icons %}
{% set iconMap = { {% set iconMap = {
bedsCount: { icon: 'bed', label: 'Anzahl Betten' }, bedsCount: { icon: 'bed', label: 'Anzahl Betten' },
@@ -167,15 +153,8 @@
</h2> </h2>
{{ ctx.hotelCmsData.roomTypes | raw }} {{ ctx.hotelCmsData.roomTypes | raw }}
{% endif %} {% endif %}
{% endblock %}
{% if ctx.hotelCmsData.additionalInformation|striptags|trim is not empty %} {% endembed %}
<h2>
Weitere Infos
</h2>
{{ ctx.hotelCmsData.additionalInformation | raw }}
{% endif %}
</div>
</div>
{{ macros.imageGallery(ctx.hotelCmsData.images.resized.l, ctx.hotelCmsData.images.resized.m, ctx.accommodation.name) }} {{ macros.imageGallery(ctx.hotelCmsData.images.resized.l, ctx.hotelCmsData.images.resized.m, ctx.accommodation.name) }}
{% endif %} {% endif %}
@@ -183,25 +162,12 @@
{# region description #} {# region description #}
{% if ctx.hotelCmsData is not null and ctx.hotelCmsData.region is not null %} {% if ctx.hotelCmsData is not null and ctx.hotelCmsData.region is not null %}
{% set region = ctx.hotelCmsData.region %} {% set region = ctx.hotelCmsData.region %}
<div {{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden', 'iconOpen': 'rotate-180' }) }}> {% embed '_partials/_collapsible.html.twig' with {
<button type="button" heading: 'Die Region ' ~ region.name,
class="flex items-center justify-between w-full text-left" id: 'region',
data-action="toggle#toggle"> region: region,
<span class="block text-2xl md:text-3xl font-bold mb-2" } %}
role="heading" {% block body %}
aria-level="2">
Die Region {{ region.name }}
</span>
<svg class="w-5 h-5 shrink-0 transition-transform"
data-toggle-target="icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256">
<rect width="256" height="256" fill="none"></rect>
<polyline points="208 96 128 176 48 96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></polyline>
</svg>
</button>
<div class="rte-content" data-toggle-target="content">
{% if region.altitude or region.length or region.lifts %} {% if region.altitude or region.length or region.lifts %}
<ul class="flex flex-wrap gap-x-6 text-sm not-prose"> <ul class="flex flex-wrap gap-x-6 text-sm not-prose">
{% if region.altitude %} {% if region.altitude %}
@@ -229,8 +195,8 @@
<h2>Aktuelles aus der Region</h2> <h2>Aktuelles aus der Region</h2>
{{ region.news | raw }} {{ region.news | raw }}
{% endif %} {% endif %}
</div> {% endblock %}
</div> {% endembed %}
{% if region.regionMaps is not null and region.regionMaps | length > 0 %} {% if region.regionMaps is not null and region.regionMaps | length > 0 %}
{% set regionMapImages = region.regionMaps | map(url => {url: url}) %} {% set regionMapImages = region.regionMaps | map(url => {url: url}) %}
@@ -239,6 +205,19 @@
{{ macros.imageGallery(region.images.resized.l ?? [], region.images.resized.m ?? [], region.name) }} {{ macros.imageGallery(region.images.resized.l ?? [], region.images.resized.m ?? [], region.name) }}
{% endif %} {% endif %}
{# additional information #}
{% if ctx.hotelCmsData is not null and ctx.hotelCmsData.additionalInformation|striptags|trim is not empty %}
{% embed '_partials/_collapsible.html.twig' with {
heading: 'Zusatzinformationen',
id: 'additional-information',
} %}
{% block body %}
{{ ctx.hotelCmsData.additionalInformation | raw }}
{% endblock %}
{% endembed %}
{% endif %}
</div> </div>
</div> </div>