Slider-Container & Slider-Item SDC — Erweiterungen
done
byte_theme:slider-container und
byte_theme:slider-item werden um drei Funktionen erweitert:
(1) Infinite Carousel mit automatischem Mobile-Disable unter 768 px,
(2) Grab-Cursor beim Hovern und Dragging (inkl. Kind-Elementen),
(3) Bleed-Prop für einen seitlichen Kartenüberlauf-Effekt. Alle Änderungen sind
rückwärtskompatibel — bestehende Canvas-Seiten, die den Slider verwenden,
ändern ihr Verhalten nicht.
Problem Statement
Redakteure und Site-Builder haben drei Anforderungen, die der Slider-Container vor dieser Story nicht erfüllen konnte:
- Infinite Carousel. Bestimmte Sektionen (z. B. Experten-Karussell) sollen ein endlos-loopierendes Carousel zeigen, bei dem das aktive Slide zentriert und Nachbar-Slides beidseitig sichtbar sind. Der bisherige Slider-Container konnte nur seitenbasiert navigieren.
- Fehlende Drag-Geste. Obwohl der Slider per Maus-Drag bedienbar war, zeigte der Cursor kein visuelles Signal. Nutzer erkannten die Drag-Funktion nicht. Besonders auf Child-Elementen (Links, Bilder) wurde der Cursor durch deren eigenen Default-Cursor überschrieben.
-
Bleed-Effekt. Eine Figma-Spec zeigt 2 Cards im Slider, deren äußere Kanten
links und rechts leicht abgeschnitten sind — ein subtiler „es gibt mehr"-Hinweis
ohne sichtbare Nachbar-Slides. Weder
peeknoch ein Custom-CSS-Override konnten diesen Effekt zuverlässig erzeugen.
Solution
Drei additive Erweiterungen am slider-container-SDC (Twig, JS, YAML);
slider-item bleibt unverändert. Alle neuen Props haben sichere Defaults
(infinite: false, bleed: none), sodass keine bestehende
Canvas-Konfiguration bricht.
Das Mobile-Disable-Pattern für Infinite stellt sicher, dass das Carousel auf schmalen Viewports (unter 768 px) immer als Paged-Slider läuft — Infinite-Geometrie ist auf kleinen Screens visuell nicht sinnvoll und erfordert eine andere Slide-Breite.
User Stories
- Als Site-Builder möchte ich einen Slider mit Prop
infinite: truekonfigurieren können, der ein endloses Karussell zeigt, damit ich Experten- oder Partner-Sektionen nach Figma-Spec umsetzen kann. - Als Besucher der Website möchte ich beim Hovern über einen Slider eine Hand-Geste sehen, damit ich erkenne, dass der Slider per Maus bedienbar ist.
- Als Site-Builder möchte ich mit dem Prop
bleed: smeinen subtilen seitlichen Überlauf-Effekt aktivieren können, damit ich das Figma-Design mit 2 Cards und abgeschnittenen Außenkanten umsetzen kann. - Als Entwickler möchte ich, dass Infinite auf Mobile automatisch deaktiviert wird, damit keine Layout-Probleme auf schmalen Viewports entstehen, ohne dass der Site-Builder das manuell konfigurieren muss.
Features im Detail
1 — Infinite Carousel
Neuer Bool-Prop infinite (Default: false). Im Infinite-Modus:
- Das aktive Slide ist zentriert; linke und rechte Nachbar-Slides sind je zur Hälfte sichtbar.
- Navigation wraps endlos — kein Disable der Pfeil-Buttons am ersten/letzten Slide.
- Die Props
loop,peekundslides_per_viewwerden ignoriert, wenn Infinite aktiv ist. - Auf Viewports < 768 px ist Infinite immer deaktiviert, unabhängig vom Prop-Wert (
data-infinite="false"im DOM).
Canvas-Registrierung: infinite ist im component.yml als boolean-Prop
mit meta:enum-Labels deklariert und erscheint im Canvas-Editor als Toggle.
2 — Cursor-Grab beim Hovern & Dragging
Der Slides-Wrapper erhält die Tailwind-Klassen cursor-grab [&_*]:cursor-grab.
Der Descendant-Selektor ist notwendig, weil Kind-Elemente (z. B. <a>-Tags
in Cards) den vom Parent geerbten Cursor mit ihrem Default überschreiben würden.
Während des Drag-Vorgangs setzt das JS data-grabbing am Container-Element.
Über den CSS-Selektor [.slider-container[data-grabbing]_&]:cursor-grabbing
und dessen [&_*]-Pendant wird der grabbing-Cursor auf dem
gesamten Slider und allen Kind-Elementen erzwungen.
Im Canvas-Editor (data-canvas-editor) sind alle Grab-Klassen deaktiviert
(cursor-default / cursor-auto auf Kind-Elementen).
3 — Bleed-Prop
Neuer Enum-Prop bleed mit den Werten none (Default) und
sm (15 px pro Seite). Bei aktivem Bleed:
- Der Viewport-Wrapper erhält negative Seitenmargins (
-15px), sodass der Slide-Inhalt 15 px über den Container hinausragt. - Das initiale
translateXwird um den Bleed-Betrag nach links korrigiert, damit der erste Slide nicht bündig an der Container-Kante sitzt, sondern ebenfalls links abgeschnitten wird. - Bleed wird ignoriert, wenn
infinite: trueoderpeek != noneaktiv ist.
Props-Schema
| Prop | Typ | Default | Enum | Canvas-Label | Neu? |
|---|---|---|---|---|---|
infinite | boolean | false | — | Infinite carousel | Neu |
bleed | string | none | none, sm | Card bleed | Neu |
peek | string | none | none, sm, md, lg | Slide peek | vorhanden |
loop | boolean | true | — | Loop | vorhanden |
slides_per_view | number | 3 | 1, 2, 3, 4 | Slides per view | vorhanden |
autoplay | boolean | false | — | Autoplay | vorhanden |
autoplay_interval | number | 4000 | 2000–8000 | Autoplay interval | vorhanden |
Technische Entscheidungen
Mobile-Disable via separatem Active-State-Feld
Das JS trennt den Prop-Wert (#infinite) vom effektiv aktiven Wert
(#infiniteActive). #infiniteActive wird in init()
aus dem Prop-Wert und einem Breakpoint-Check berechnet. Alle Methoden arbeiten
ausschließlich mit #infiniteActive.
Ein resize-Listener aktualisiert #infiniteActive bei
Viewport-Änderungen (z. B. Rotation von Portrait → Landscape auf Tablets).
Alternative verworfen: CSS-Media-Query allein würde JS-Logik (Clone-Handling,
Slide-Breite) nicht deaktivieren — JS muss den Zustand kennen.
data-grabbing statt inline-style
Das JS setzt data-grabbing am Container-Element statt
track.style.cursor = "grabbing". Der Inline-Style würde zwar den Track
selbst betreffen, aber nicht die Descendant-Selektor-Cascade für Kind-Elemente
auslösen. Das data-grabbing-Attribut am äußersten Container erlaubt
den vollen CSS-Selektorzugriff auf alle Nachfahren.
Bleed-Korrektur am translateX
Ohne Korrektur liegt der erste Slide bündig am linken Viewport-Rand, da der Track
bei translateX(0) startet und die negativen Margins den Viewport nach
außen erweitern, nicht nach innen. Die Formel lautet:
translateX = peek_px - bleed_px - (current * slideStep)
Das subtrahierte bleed_px verschiebt den Track beim initialen Render
um 15 px nach links, sodass der erste Slide denselben Clip wie alle anderen erhält.
Testing
Kernel-Render-Contract-Tests in
tests/src/Kernel/SliderComponentTest.php (#[Group('slider')]).
Gepinnt werden strukturelle DOM-Elemente und data-*-Attribute — kein
visuelles Styling.
| Test | Prüft |
|---|---|
testSliderContainerRendersStructure | .slider-container, .slider__track, .slider__slides, data-loop, data-slides-per-view, data-peek |
testSliderContainerAlwaysRendersHeaderRow | .slider__header, .slider__prev, .slider__next, .slider__counter |
testSliderContainerHeaderShowsHeadlineSlot | Slot-Content landet in .slider__headline |
testSliderContainerPeekPropSetsDataAttribute | data-peek="md", overflow-hidden auf Viewport |
testSliderItemRendersAsSlide | .slider__slide |
testSliderContainerInfiniteDataAttribute | data-infinite="true" bei infinite: true |
testSliderContainerInfiniteDefaultsFalse | data-infinite="false" ohne Prop |
Ausführen: ddev exec ./vendor/bin/phpunit --group slider
Out of Scope
- Autoplay-Pause bei Hover — wurde nicht angefragt; bestehende Autoplay-Logik unverändert.
- Swipe-Gesten auf Touch (Mobile) — Touch-Events sind bereits implementiert; kein neues Scope.
- Infinite + Peek kombiniert — Infinite deaktiviert Peek (dokumentiert in
component.yml). - Animiertes Infinite-Cloning (GSAP, CSS-Keyframes) — Standard CSS-Transition reicht für den Use-Case.
- slider-item Änderungen — nicht Teil dieser Story.