Skip to content

Commit a5e87bf

Browse files
authored
Merge pull request #3106 from exadel-inc/feat/esl-carousel-css
epic: CSS Based ESLCarousel renderers
2 parents 064e71f + 357e2b8 commit a5e87bf

14 files changed

+372
-12
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
title: Fade Carousel (CSS)
3+
order: -1
4+
tags: carousel-sample
5+
---
6+
7+
<div class="esl-carousel-no-extra-space esl-carousel-nav-container">
8+
<button type="button" class="esl-carousel-arrow inner prev hide-xs" esl-carousel-nav="group:prev">
9+
<span class="sr-only">Previous Slide</span>
10+
</button>
11+
12+
<esl-carousel demo-options-target
13+
type="css-fade"
14+
esl-carousel-touch=""
15+
loop="true">
16+
<ul esl-carousel-slides>
17+
{% for i in range(0, 4) -%}
18+
<li esl-carousel-slide {{ 'active' if loop.first }}>
19+
<div class="card">
20+
<div class="img-container img-container-16-9" esl-image-container>
21+
<img class="img-fade img-cover"
22+
alt="{{ 'Carousel slide ' + loop.index }}"
23+
src="{{ '/assets/carousel/' + loop.index + '-sm.jpg' | url }}"
24+
loading="lazy" />
25+
<div class="h1 text-slide text-white">Slide {{ loop.index }}</div>
26+
</div>
27+
</div>
28+
</li>
29+
{%- endfor %}
30+
</ul>
31+
</esl-carousel>
32+
33+
<button type="button" class="esl-carousel-arrow inner next hide-xs" esl-carousel-nav="group:next">
34+
<span class="sr-only">Next Slide</span>
35+
</button>
36+
37+
<esl-carousel-dots class="carousel-dots-wrapper"></esl-carousel-dots>
38+
39+
<style>
40+
.text-slide {
41+
position: absolute;
42+
top: 50%;
43+
left: 50%;
44+
transform: translate(-50%, -50%);
45+
}
46+
</style>
47+
</div>

packages/esl/src/esl-carousel/README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,10 @@ To fine-tune the layout you can use the following recipes:
185185
To define the space between the slides you can just use native flexbox `gap` property.
186186
The renderer is aware of the gap and will adjust the slide size and calculations accordingly.
187187
3. Transitions:
188-
The renderer utilizes JS Animation API to animate the slides. The only way to customize the transition timing is to use the `step-duration` attribute on the `esl-carousel` element.
188+
The renderer uses the JavaScript Animation API to animate slides. The only way to customize the transition is by setting the duration via the CSS variable `--esl-carousel-step-duration`.
189+
The carousel renderer reads the computed value of this CSS variable to determine the transition duration.
190+
In addition to using the CSS variable directly, you can also specify the `step-duration` attribute on the `<esl-carousel>` element. This attribute supports `ESLMediaRuleList` syntax, including definitions based on `media` attribute.
191+
Technically, the `step-duration` attribute sets the transition duration CSS variable on the carousel root element during the animation step.
189192
4. Siblings visibility and overflow:
190193
The CSS overflow property is set to `clip`(or `hidden` for legacy browsers) on the `esl-carousel` element to hide out of view slides.
191194
However, the start position and calculations are based on `esl-carousel-slides` container element.
@@ -197,12 +200,42 @@ To fine-tune the layout you can use the following recipes:
197200
Note that if you use siblings visibility effect, you will not see the last slide before the first one in the loop mode unless backward animation is playing.
198201
However, this option could be useful to limit CLS issues, when content of the slide is heavy (e.g. `esl-media` with the fill option).
199202

203+
- #### Centered (type: `centered`) Renderer
204+
An extension of the default renderer that centers the active slide in the carousel.
205+
Does not have any critical configurational or functional differences from the default renderer except the process of offset calculation.
206+
200207
- #### Grid (type: `grid`) Renderer <i class="badge badge-sup badge-warning">beta</i>
201208
The grid renderer for ESL Carousel is based on the Default renderer but uses the CSS Grid layout to display slides.
202209
Unlike the Default renderer, the Grid renderer displays multiple rows (horizontal case) or columns (vertical case) of slides.
203210

204211
Note that the Grid renderer is more restrictive in terms of the slide size definition. Unlike the Default renderer, the Grid renderer does not support relative sizes for the slides (grid layout liitations).
205212

213+
#### Default CSS Renderer (type: `css`)
214+
Uses the `ESLCSSCarouselRenderer` implementation.
215+
This renderer does not apply any JavaScript-based animation logic. It relies entirely on CSS-defined transitions and animations.
216+
217+
> **Note:** This renderer does not listen for specific transition or animation events. Therefore, the animation duration must be explicitly defined using the `--esl-carousel-step-duration` CSS variable.
218+
It is also good practice to use the `--esl-carousel-step-duration` CSS variable within your animation. This enables duration customization via the `step-duration` attribute as well.
219+
220+
Animations can make use of global markers such as `active`, `pre-active`, `next`, and `prev` attributes, as well as the `animating` state attribute on the carousel element.
221+
222+
Additionally, the renderer provides the CSS classes `left`, `right`, `forward`, and `backward` on affected slides to help define animation direction.
223+
224+
The renderer supports the move operation, but you must rely on one of the following CSS variables and markers:
225+
* `shifted` – A root element attribute added to the carousel element when the move operation is not committed.
226+
* `--esl-carousel-offset` – A CSS variable representing the move offset in pixels.
227+
* `--esl-carousel-offset-ratio` – A CSS variable representing the offset relative to the carousel size and move tolerance.
228+
229+
By default, this renderer does not include any specific animation. However, several built-in animation implementations are provided (detailed below).
230+
231+
#### CSS Fade Renderer (type: `css-fade`)
232+
An extension of the default CSS renderer that provides fade animation styles for slide transitions.
233+
Supports move operations relative to the carousel size and move tolerance.
234+
235+
#### CSS Slide Renderer (type: `css-slide`)
236+
An extension of the default CSS renderer that provides sliding animation styles for slide transitions.
237+
Supports move operations based on absolute pixel offset.
238+
206239
- #### None (type: `none`) Renderer
207240
The none renderer for ESL Carousel is a dummy renderer that does not change the slides layout and assumes that all the slides are visible at once.
208241
It is useful when you want to 'mute' the carousel behavior for some media conditions.

packages/esl/src/esl-carousel/core.less

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@
33
// Renderer Default
44
@import './renderers/esl-carousel.default.renderer.less';
55
@import './renderers/esl-carousel.grid.renderer.less';
6+
@import './renderers/esl-carousel.css.renderer.less';
7+
@import './renderers/esl-carousel.css.fade.renderer.less';
8+
@import './renderers/esl-carousel.css.slide.renderer.less';

packages/esl/src/esl-carousel/core.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@ import './renderers/esl-carousel.none.renderer';
3333
import './renderers/esl-carousel.default.renderer';
3434
import './renderers/esl-carousel.grid.renderer';
3535
import './renderers/esl-carousel.centered.renderer';
36+
import './renderers/esl-carousel.css.renderer';
37+
import './renderers/esl-carousel.css.fade.renderer';
38+
import './renderers/esl-carousel.css.slide.renderer';

packages/esl/src/esl-carousel/core/esl-carousel.less

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
:root {
22
--esl-slide-initial-size: 100%;
33
--esl-carousel-side-space: 5px;
4+
--esl-carousel-step-duration: 250ms;
45
}
56

67
// CORE styles

packages/esl/src/esl-carousel/core/esl-carousel.renderer.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {memoize} from '../../esl-utils/decorators';
22
import {isEqual} from '../../esl-utils/misc/object';
3+
import {parseTime} from '../../esl-utils/misc/format';
4+
import {promisifyTimeout} from '../../esl-utils/async/promise/timeout';
35
import {SyntheticEventTarget} from '../../esl-utils/dom';
46
import {ESLCarouselDirection} from './esl-carousel.types';
57
import {ESLCarouselSlideEvent} from './esl-carousel.events';
@@ -10,6 +12,9 @@ import type {ESLCarouselSlideEventInit} from './esl-carousel.events';
1012
import type {ESLCarouselActionParams, ESLCarouselConfig, ESLCarouselNavInfo} from './esl-carousel.types';
1113

1214
export abstract class ESLCarouselRenderer implements ESLCarouselConfig {
15+
/** CSS variable name to set transition duration */
16+
public static readonly TRANSITION_DURATION_PROP = '--esl-carousel-step-duration';
17+
1318
public static is: string;
1419
public static classes: string[] = [];
1520

@@ -63,6 +68,29 @@ export abstract class ESLCarouselRenderer implements ESLCarouselConfig {
6368
return this.$carousel.$slides || [];
6469
}
6570

71+
protected get animating(): boolean {
72+
return this.$carousel.hasAttribute('animating');
73+
}
74+
protected set animating(value: boolean) {
75+
this.$carousel.toggleAttribute('animating', value);
76+
}
77+
78+
protected get transitionDuration(): number {
79+
const name = ESLCarouselRenderer.TRANSITION_DURATION_PROP;
80+
const duration = getComputedStyle(this.$area).getPropertyValue(name);
81+
return parseTime(duration);
82+
}
83+
protected set transitionDuration(value: number | null) {
84+
if (typeof value === 'number' && value > 0) {
85+
this.$carousel.style.setProperty(ESLCarouselRenderer.TRANSITION_DURATION_PROP, `${value}ms`);
86+
} else {
87+
this.$carousel.style.removeProperty(ESLCarouselRenderer.TRANSITION_DURATION_PROP);
88+
}
89+
}
90+
protected get transitionDuration$$(): Promise<void> {
91+
return promisifyTimeout(this.transitionDuration);
92+
}
93+
6694
public equal(config: ESLCarouselConfig): boolean {
6795
return isEqual(this.config, config);
6896
}
@@ -117,13 +145,15 @@ export abstract class ESLCarouselRenderer implements ESLCarouselConfig {
117145
this.$carousel.dispatchEvent(ESLCarouselSlideEvent.create('CHANGE', details));
118146
this.setPreActive(index);
119147

148+
this.transitionDuration = params.stepDuration;
120149
try {
121150
await this.onBeforeAnimate(index, direction, params);
122151
await this.onAnimate(index, direction, params);
123152
await this.onAfterAnimate(index, direction, params);
124153
} catch (e: unknown) {
125154
console.error(e);
126155
}
156+
this.transitionDuration = null;
127157

128158
this.setActive(index, {direction, ...params});
129159
}

packages/esl/src/esl-carousel/core/esl-carousel.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {ESLBaseElement} from '../../esl-base-element/core';
33
import {attr, boolAttr, ready, decorate, listen, memoize} from '../../esl-utils/decorators';
44
import {isMatches} from '../../esl-utils/dom/traversing';
55
import {microtask} from '../../esl-utils/async';
6-
import {parseBoolean, parseTime, sequentialUID} from '../../esl-utils/misc';
6+
import {parseBoolean, parseTime, sequentialUID, toCamelCase} from '../../esl-utils/misc';
77

88
import {CSSClassUtils} from '../../esl-utils/dom/class';
99
import {ESLTraversingQuery} from '../../esl-traversing-query/core';
@@ -47,7 +47,7 @@ export class ESLCarousel extends ESLBaseElement {
4747
@attr({defaultValue: 'false'}) public vertical: string | boolean;
4848

4949
/** Duration of the single slide transition */
50-
@attr({defaultValue: '250'}) public stepDuration: string;
50+
@attr() public stepDuration: string;
5151

5252
/** Container selector (supports traversing query). Carousel itself by default */
5353
@attr({defaultValue: ''}) public container: string;
@@ -143,7 +143,7 @@ export class ESLCarousel extends ESLBaseElement {
143143
memoize.clear(this, '$container');
144144
return this.updateStateMarkers();
145145
}
146-
memoize.clear(this, `${attrName}Rule`);
146+
memoize.clear(this, `${toCamelCase(attrName)}Rule`);
147147
this.update();
148148
}
149149

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
.esl-carousel-css-renderer.esl-carousel-css-fade {
2+
&[animating] [esl-carousel-slide] {
3+
transition: opacity var(--esl-carousel-step-duration) linear;
4+
}
5+
6+
[esl-carousel-slide] {
7+
opacity: 0;
8+
}
9+
[esl-carousel-slide][active] {
10+
opacity: 1;
11+
}
12+
[esl-carousel-slide][pre-active] {
13+
position: absolute;
14+
top: 0;
15+
width: 100%;
16+
}
17+
&[animating] [esl-carousel-slide][pre-active] {
18+
z-index: 2;
19+
opacity: 1;
20+
}
21+
22+
&[shifted] [esl-carousel-slide][pre-active] {
23+
z-index: 2;
24+
opacity: var(--esl-carousel-offset-ratio, 0);
25+
}
26+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {ESLCarouselRenderer} from '../core/esl-carousel.renderer';
2+
import {ESLCSSCarouselRenderer} from './esl-carousel.css.renderer';
3+
4+
@ESLCarouselRenderer.register
5+
export class ESLCSSFadeCarouselRenderer extends ESLCSSCarouselRenderer {
6+
public static override is = 'css-fade';
7+
public static override classes: string[] = [
8+
...ESLCSSCarouselRenderer.classes,
9+
'esl-carousel-css-fade'
10+
];
11+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
.esl-carousel-css-renderer {
2+
[esl-carousel-slides] {
3+
display: block;
4+
position: relative;
5+
width: 100%;
6+
}
7+
8+
[esl-carousel-slide] {
9+
position: relative;
10+
display: none;
11+
backface-visibility: hidden;
12+
}
13+
14+
[esl-carousel-slide][active],
15+
[esl-carousel-slide][pre-active] {
16+
display: block;
17+
}
18+
}

0 commit comments

Comments
 (0)