Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

epic: esl-animate documentation, mixin and cleanup (#1281) #1736

Merged
merged 22 commits into from
Jun 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9f9c156
feat(esl-animate): animate mixin element
fshovchko Oct 3, 2022
b025974
docs(esl-animate): update styling
fshovchko Oct 3, 2022
d94da21
Merge branch 'main-beta' into feat/animate-mixin
fshovchko Oct 6, 2022
9671478
chore(esl-animate): remove observed attributes
fshovchko Oct 8, 2022
42598c1
docs(esl-animate): update styling
fshovchko Oct 8, 2022
4aee65b
chore(esl-animate): add mixin default config
fshovchko Oct 15, 2022
9ad6c91
Merge pull request #1266 from exadel-inc/feat/animate-mixin
ala-n Oct 19, 2022
aa5d8be
test(esl-animate): cover module parts with tests
fshovchko Nov 6, 2022
4b8461c
docs(esl-animate): esl-animate documentation
fshovchko Nov 6, 2022
85cd7c8
test(esl-animate): test refactoring
fshovchko Nov 9, 2022
0878c54
docs(esl-animate): apply suggestions from review
fshovchko Nov 10, 2022
443de83
test(esl-animate): apply suggestions from code review
fshovchko Nov 15, 2022
d8ff09f
Merge pull request #1326 from exadel-inc/docs/esl-animate-documentation
ala-n Nov 21, 2022
d7d5f8e
Merge pull request #1325 from exadel-inc/test/esl-animate-test-coverage
ala-n Nov 22, 2022
3a16f31
Merge branch 'main' into epic/esl-animate-4.0.0
ala-n Jun 22, 2023
b008155
style(esl-animate): fix code style (override keyword)
ala-n Jun 22, 2023
89403e1
refactor(esl-animate): refactor service implementation
ala-n Jun 23, 2023
0fdbaf3
refactor(esl-animate): refactor mixin implementation and types
ala-n Jun 23, 2023
3f34cbe
test(esl-utils): cleanup for mock IntersectionObserver implementation
ala-n Jun 23, 2023
9889e4e
docs(esl-animate): cleanup for documentation and example
ala-n Jun 23, 2023
a7a9503
docs(esl-animate): apply small documentation fixes
ala-n Jun 25, 2023
f4fc7e1
Merge branch 'main' into epic/esl-animate-4.0.0
ala-n Jun 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pages/src/localdev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
ESLFootnotes,
ESLTooltip,
ESLAnimate,
ESLAnimateMixin,
ESLShare,
ESLRelatedTarget
} from '../../src/modules/all';
Expand Down Expand Up @@ -101,6 +102,7 @@ ESLNote.register();
ESLTooltip.register();

ESLAnimate.register();
ESLAnimateMixin.register();

ESLShare.config(() => fetch('/assets/share/config.json').then((response) => response.json()));
ESLShare.register();
Expand Down
27 changes: 18 additions & 9 deletions pages/views/examples/animate.njk
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ aside:
<h2>Simple animation</h2>

<div class="simple-grid simple-grid-fw">
{% for i in range(0, 4) -%}
<esl-animate class="esl-animate-fade simple-grid-cell bg-green round">
{% for i in range(0, 2) -%}
<esl-animate repeat class="esl-animate-fade simple-grid-cell bg-green round text-white h3 text-center p-4">
esl-animate-fade
</esl-animate>
<esl-animate class="esl-animate-slide-up simple-grid-cell bg-green round">
<esl-animate repeat class="esl-animate-slide-up simple-grid-cell bg-green round text-white h3 text-center p-4">
esl-animate-slide-up
</esl-animate>
<esl-animate class="esl-animate-slide-left simple-grid-cell bg-green round">
<esl-animate repeat class="esl-animate-slide-left simple-grid-cell bg-green round text-white h3 text-center p-4">
esl-animate-slide-left
</esl-animate>
<esl-animate class="esl-animate-slide-down simple-grid-cell bg-green round">
<esl-animate repeat class="esl-animate-slide-down simple-grid-cell bg-green round text-white h3 text-center p-4">
esl-animate-slide-down
</esl-animate>
<esl-animate class="esl-animate-slide-right simple-grid-cell bg-green round">
<esl-animate repeat class="esl-animate-slide-right simple-grid-cell bg-green round text-white h3 text-center p-4">
esl-animate-slide-right
</esl-animate>
{%- endfor %}
Expand All @@ -37,9 +37,18 @@ aside:
<h2>Grouped and repetitive animation</h2>
<ul class="round simple-grid">
<esl-animate target="::parent::child(li)" group repeat></esl-animate>
{% for i in range(0, 72) -%}
<li class="bg-blue round esl-animate-slide-up simple-grid-cell {{ 'hide-xs' if i >= 12 }}"
is="my-comp my-com-2"></li>
{% for i in range(0, 36) -%}
<li class="bg-blue round esl-animate-slide-up simple-grid-cell {{ 'hide-xs' if i >= 12 }}"></li>
{%- endfor %}
</ul>
</section>

<section>
<h2>Grouped animation with custom delay by mixin</h2>
<ul class="round simple-grid">
{% for i in range(0, 36) -%}
<li class="bg-orange round esl-animate-slide-left simple-grid-cell {{ 'hide-xs' if i >= 12 }}"
esl-animate="{group: true, groupDelay: 50}"></li>
{%- endfor %}
</ul>
</section>
140 changes: 94 additions & 46 deletions src/modules/esl-animate/README.md
Original file line number Diff line number Diff line change
@@ -1,61 +1,80 @@
# [ESL](https://exadel-inc.github.io/esl/) Animate

Version: *1.0.0*.
Version: *2.0.0*.

Authors: *Anna-Mariia Petryk*, *Alexey Stsefanovich (ala'n)*, *Julia Murashko*.

**_Important Notice: the component is under beta version, it is tested and ready to use but be aware of its potential critical API changes._**
Authors: *Anna-Mariia Petryk*, *Feoktyst Shovchko*, *Alexey Stsefanovich (ala'n)*, *Julia Murashko*.

<a name="intro"></a>

**ESLAnimate** is a module to animate items on viewport intersecting.
`esl-animate` is a module that provides service and its DOM API to animate elements on their intersection with the viewport

The module consists of JS API `ESLAnimateService`, Custom element `ESLAnimate`, and Mixin element `ESLAnimateMixin`.
ESLAnimateService is a core of the `esl-animate` module. Element needs to be observed by ESLAnimateService in order to be
animated.

### Module Features:
- Add class(es) when observed elements enter the viewport area
- Animate in a group, that allows adding an animation delay for each next item in the intersection queue
- Automatic re-animation after the item exits the viewport area
- Pre-defined CSS animations
- `esl-animate-fade`
- `esl-animate-slide-left`
- `esl-animate-slide-right`
- `esl-animate-slide-up`
- `esl-animate-slide-down`

Features:
- Add class(es) when observed elements enter viewport area
- Support group animation that allows item delay its animation on the passed time after previously animated item
- Support forward and backward animations directions
- Support automatic re-animate after item exit viewport area
- JS API (Service) + Custom Element - Plugin for simple initialization
## `ESLAnimateService`

## ESLAnimateService
ESLAnimateService is a core of esl-animate module. Element needs to be observed by ESLAnimateService
in order to be animated.
**ESLAnimateService** provides a way to asynchronously add animation on the intersection of a target element with a viewport. It is based on [Intersection Observer Api](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) and serves as a core functionality for `ESLAnimate` and `ESLAnimateMixin` elements.

### Service static and instance API:

### Service API:
- `ESLAnimateService.observe(els, config)` - method to start element observation
- `els` - Element or array of Elements to observe and animate
- `config` - an optional ESLAnimateConfig object to describe the behavior of the animation functionality
- `els` - element or array of elements to observe and animate
- `config` - optional ESLAnimateConfig object to describe the behavior of the animation functionality
- `ESLAnimateService.unobserve(els)` - method to unsubscribe ESLAnimateService from observing elements
- `ESLAnimateService.isObserved(el)` - check if element observed by ESLAnimateService

### ESLAnimateConfig (Configuration API)
- `cls` - CSS class or classes to control animation (`in` by default)
(supports ESL extended class definition syntax, [CSSClassUtil](../esl-utils/dom/class.ts))
- `group` (boolean) - enable group animation for items
(item will start animation with a delay after previous item animation start)
- `groupDelay` - number of milliseconds to delay animation in group
- `ESLAnimateService.isObserved(el)` - check if the element is observed by ESLAnimateService

You can also create a separate (from global) `ESLAnimateService` instance by calling its constructor.

### Configuration API: `ESLAnimateConfig`
- `cls` (`in` by default) - CSS class(es) to control animation.
The control class(es) will be added to the observed element(s) after they had intersected with the viewport area.
Service supports ESL extended class definition syntax, [CSSClassUtils](../esl-utils/dom/class.ts)
- `group` (boolean) - enable group animation for items, hence take `groupDelay` value into account while performing
animation (item will start the animation with a delay after the previous item's animation start)
- `groupDelay` (`100` by default) - number of milliseconds animation delay in group
- `repeat` (boolean) - refresh (re-animate) items when they became invisible (exit viewport)
- `force` (boolean) - if true then allows to re-animate items when ESLAnimateService subscribed
on already animated item
- `ratio` (0.2|0.4|0.6|0.8) - intersection ratio to consider element as visible.
Only 0.2 (20%), 0.4 (40%), 0.6 (60%), 0.8 (80%) values are allowed due to share of IntersectionObserver instance
with a fixed set of thresholds defined.

## ESLAnimate
ESLAnimate (`<esl-animate>`) - custom element to automatically initialize ESLAnimateService from html

### Element API
- `target` - target element or elements to animate, defined by [ESLTraversingQuery](../esl-traversing-query/README.md)
Default: empty (animates itself)
- `cls` - CSS class or classes to control animation (`in` by default)
(supports ESL extended class definition syntax, [CSSClassUtil](../esl-utils/dom/class.ts))
- `group` (boolean) - enable group animation for items
(item will start animation with a delay after previous item animation start)
- `group-delay` - number of milliseconds to delay animation in group
- `force` (boolean) - allows to re-animate items when ESLAnimateService subscribed
on already animated item if set to true
- `ratio` (0.2|0.4|0.6|0.8) - intersection ratio to consider an element as visible
Only 0.2 (20%), 0.4 (40%), 0.6 (60%), 0.8 (80%) values are allowed due to sharing of IntersectionObserver instance
with a fixed set of thresholds defined

## `ESLAnimate` custom element

**ESLAnimate** is a custom element that subscribes `ESLAnimateService` to elements from html.

To use ESLAnimate you need to include the following code:
```js
ESLAnimate.register();
```

### `ESLAnimate` Attributes | Properties:
- `target` - target element(s) to animate, defined by [ESLTraversingQuery](../esl-traversing-query/README.md). By default target value is empty, meaning component will animate itself
- `cls` (`in` by default) - CSS class(es) to control animation. The control class(es) will be added to observed element(s), after they had intersected with vieport area. Service supports ESL extended class definition syntax, [CSSClassUtils](../esl-utils/dom/class.ts)
- `group` (boolean) - enable group animation for items, hence take `groupDelay` value into account while performing
animation (item will start the animation with a delay after the previous item's animation start)
- `groupDelay` (`100` by default) - number of milliseconds animation delay in group
- `repeat` (boolean) - refresh (re-animate) items when they became invisible (exit viewport)
- `ratio` - number of intersection ratio to consider element as visible
Only 0.2 (20%), 0.4 (40%), 0.6 (60%), 0.8 (80%) values are allowed due to share of IntersectionObserver instance
with a fixed set of thresholds defined.
- `force` (boolean) - allows to re-animate items when ESLAnimateService subscribed
on already animated item if set to true
- `ratio` (0.2|0.4|0.6|0.8) - intersection ratio to consider an element as visible
Only 0.2 (20%), 0.4 (40%), 0.6 (60%), 0.8 (80%) values are allowed due to the share of the IntersectionObserver instance
with a fixed set of thresholds defined

By default, attributes `group`, `repeat`, and `target` are observed, meaning the animation sequence will restart once
theese attributes are changed. Additionally, you can do the re-animation manually by calling the instance method `reanimate()`.

### Use cases
- plugin (target attribute defined)
Expand All @@ -67,11 +86,40 @@ with a fixed set of thresholds defined.
...
</ul>
```
Note: `<esl-animate>` hidden (`display: none`) by default when there is no content inside
**Note: `<esl-animate>` hidden (`display: none`) by default when there is no content inside**

- wrapper (no target attribute defined)
```html
<esl-animate class="esl-animate-fade" cls="in">
...HTML Content...
</esl-animate>
```

## `ESLAnimateMixin`

**ESLAnimateMixin** is an ESL mixin attribute that automatically subscribes `ESLAnimateService` to the element from html.

To use ESLAnimateMixin you need to include the following code:
```js
ESLAnimateMixin.register();
```

### `ESLAnimateMixin` Attributes | Properties:
- `esl-animate-mixin` - json attribute containing following properties:
- `cls` (`in` by default) - CSS class(es) to control animation. The control class(es) will be added to observed element(s), after they had intersected with vieport area. Service supports ESL extended class definition syntax, [CSSClassUtils](../esl-utils/dom/class.ts)
- `repeat` (boolean) - refresh (re-animate) items when they became invisible (exit viewport)
- `ratio` (0.2|0.4|0.6|0.8) - intersection ratio to consider an element as visible
Only 0.2 (20%), 0.4 (40%), 0.6 (60%), 0.8 (80%) values are allowed due to sharing of the IntersectionObserver instance
with a fixed set of thresholds defined

Apart from `ESLAnimate` module mixin doesn't observe any of the element attributes. But you can do the re-animation manually by calling instance method `reanimate()`.

### Use cases
- default declaration
```html
<div esl-animate>...HTML Content...</div>
```
- custom config
```html
<div esl-animate="{repeat: true, ratio: 0.2, cls: 'in'}">...HTML Content...</div>
```
1 change: 1 addition & 0 deletions src/modules/esl-animate/core.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type {ESLAnimateShape} from './core/esl-animate.shape';

export * from './core/esl-animate';
export * from './core/esl-animate-mixin';
export * from './core/esl-animate-service';
50 changes: 50 additions & 0 deletions src/modules/esl-animate/core/esl-animate-mixin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {ESLMixinElement} from '../../esl-mixin-element/core';
import {ready, jsonAttr} from '../../esl-utils/decorators';
import {ExportNs} from '../../esl-utils/environment/export-ns';

import {ESLAnimateService} from './esl-animate-service';
import type {ESLAnimateConfig} from './esl-animate-service';

/**
* ESLAnimateMixin - custom mixin element for quick {@link ESLAnimateService} attaching
*
* Use example:
* `<div esl-animate>Content</div>`
*
* Supports additional parameters:
* `<div esl-animate={repeat: true, ratio: 0.8, cls: 'in'}>Content</div>`
*/
@ExportNs('AnimateMixin')
export class ESLAnimateMixin extends ESLMixinElement {
public static override is = 'esl-animate';

public static defaultConfig: ESLAnimateConfig = {
force: true
};

@jsonAttr({name: ESLAnimateMixin.is})
public options?: ESLAnimateConfig;

protected mergeDefaultParams(): ESLAnimateConfig {
const type = this.constructor as typeof ESLAnimateMixin;
return Object.assign({}, type.defaultConfig, this.options);
}

@ready
public override connectedCallback(): void {
super.connectedCallback();
this.reanimate();
}

@ready
public override disconnectedCallback(): void {
super.disconnectedCallback();
ESLAnimateService.unobserve(this.$host);
}

/** Reinitialize {@link ESLAnimateService} for target */
public reanimate(): void {
ESLAnimateService.unobserve(this.$host);
ESLAnimateService.observe(this.$host, this.mergeDefaultParams());
}
}
41 changes: 19 additions & 22 deletions src/modules/esl-animate/core/esl-animate-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ export interface ESLAnimateConfig {
}

/** ESLAnimateService animation inner options. Contains system animation properties */
interface ESLAnimateConfigInner extends Required<ESLAnimateConfig> {
interface ESLAnimateConfigInner extends ESLAnimateConfig {
// Required parts
cls: string;
ratio: number;
groupDelay: number;

/** (private) animation requested */
_timeout?: number;
/** (private) marker to unobserve */
Expand All @@ -43,7 +48,7 @@ interface ESLAnimateConfigInner extends Required<ESLAnimateConfig> {
export class ESLAnimateService {

/** ESLAnimateService default animation configuration */
protected static DEFAULT_CONFIG: ESLAnimateConfig = {cls: 'in', groupDelay: 100, ratio: 0.4};
protected static DEFAULT_CONFIG: ESLAnimateConfigInner = {cls: 'in', groupDelay: 100, ratio: 0.4};
/** ESLAnimationService IntersectionObserver properties */
protected static OPTIONS_OBSERVER: IntersectionObserverInit = {threshold: [0.001, 0.2, 0.4, 0.6, 0.8]};

Expand All @@ -63,7 +68,7 @@ export class ESLAnimateService {

/** @returns if service observing target */
public static isObserved(target: Element): boolean {
return !!this.instance.getConfigFor(target);
return this.instance.isObserved(target);
}

@memoize()
Expand All @@ -83,7 +88,8 @@ export class ESLAnimateService {
* @param config - optional animation configuration
*/
public observe(el: Element, config: ESLAnimateConfig = {}): void {
const cfg = this.setConfigFor(el, config);
const cfg = Object.assign({}, ESLAnimateService.DEFAULT_CONFIG, config);
this._configMap.set(el, cfg);
cfg.force && CSSClassUtils.remove(el, cfg.cls);
this._io.observe(el);
}
Expand All @@ -94,11 +100,16 @@ export class ESLAnimateService {
this._configMap.delete(el);
}

/** @returns if service observing target */
public isObserved(target: Element): boolean {
return !!this._configMap.get(target);
}

/** Intersection observable callback */
@bind
protected onIntersect(entries: IntersectionObserverEntry[]): void {
entries.forEach(({target, intersectionRatio, isIntersecting}: IntersectionObserverEntry) => {
const config = this.getConfigFor(target);
const config = this._configMap.get(target);
if (!config) return;

// Item will be marked as visible in case it intersecting to the viewport with a ratio grater then passed visibleRatio
Expand All @@ -123,7 +134,7 @@ export class ESLAnimateService {
protected onAnimate(): void {
let time = -1;
this._entries.forEach((target) => {
const config = this.getConfigFor(target);
const config = this._configMap.get(target);
if (!config) return;

if (config._timeout) window.clearTimeout(config._timeout);
Expand All @@ -139,26 +150,12 @@ export class ESLAnimateService {

/** Animates passed item */
protected onAnimateItem(item: Element): void {
const config = this.getConfigFor(item);
const config = this._configMap.get(item);
if (!config) return;

CSSClassUtils.add(item, config.cls);
this._entries.delete(item);

if (config._unsubscribe) {
this._io.unobserve(item);
this._configMap.delete(item);
}
}

/** Returns config */
protected getConfigFor(el: Element): ESLAnimateConfigInner | undefined {
return this._configMap.get(el);
}
/** Returns config */
protected setConfigFor(el: Element, config: ESLAnimateConfig): ESLAnimateConfigInner {
const cfg = Object.assign({}, ESLAnimateService.DEFAULT_CONFIG, config) as ESLAnimateConfigInner;
this._configMap.set(el, cfg);
return cfg;
if (config._unsubscribe) this.unobserve(item);
}
}
Loading