@exadel/esl
Version:
Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components
210 lines (209 loc) • 8.75 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var ESLCarouselNavDots_1;
import { ExportNs } from '../../../esl-utils/environment/export-ns';
import { format } from '../../../esl-utils/misc/format';
import { ARROW_LEFT, ARROW_RIGHT } from '../../../esl-utils/dom/keys';
import { attr, listen, memoize, prop, ready } from '../../../esl-utils/decorators';
import { ESLBaseElement } from '../../../esl-base-element/core';
import { ESLTraversingQuery } from '../../../esl-traversing-query/core';
import { indexToGroup } from '../../core/esl-carousel.utils';
import { ESLCarouselChangeEvent, ESLCarouselSlideEvent } from '../../core/esl-carousel.events';
/**
* {@link ESLCarousel} Dots navigation element
*
* Example:
* ```
* <esl-carousel-dots></esl-carousel-dots>
* ```
*/
let ESLCarouselNavDots = ESLCarouselNavDots_1 = class ESLCarouselNavDots extends ESLBaseElement {
/** Default dot template implementation (readonly) */
static defaultDotBuilder(index, { tabIndex, dotLabelFormat, targetID }) {
const dot = document.createElement('button');
dot.className = 'esl-carousel-dot';
dot.setAttribute('role', 'tab');
dot.setAttribute('tabindex', tabIndex >= 0 ? '-1' : '0');
dot.setAttribute('aria-label', format(dotLabelFormat, { index: index + 1 }));
dot.setAttribute('aria-controls', targetID);
return dot;
}
/** Default dot updater implementation (readonly)*/
static defaultDotUpdater($dot, index, { activeIndex }) {
const isActive = index === activeIndex;
$dot.toggleAttribute('active', isActive);
$dot.setAttribute('aria-selected', String(isActive));
$dot.setAttribute('aria-current', String(isActive));
}
// TODO: implement in future
// /** Use arrow keys to navigate */
// @attr({defaultValue: true, parser: parseBoolean})
// public keyboardArrows: boolean;
/**
* Dots number according carousel config.
* Will be 0 if carousel does not require dots (carousel incomplete).
* (Note: memoization used during update stage)
*/
get count() {
var _a;
if (!((_a = this.$carousel) === null || _a === void 0 ? void 0 : _a.renderer))
return 0;
const { count, size } = this.$carousel.state;
const value = Math.ceil(size / count);
return value > 1 ? value : 0;
}
/** Active dot index according to carousel config. (Note: memoization used during update stage) */
get activeIndex() {
var _a;
if (!((_a = this.$carousel) === null || _a === void 0 ? void 0 : _a.renderer))
return 0;
const { activeIndex, count, size } = this.$carousel.state;
return indexToGroup(activeIndex, count, size);
}
/** Previous dot index (cycled) */
get prevIndex() {
return this.activeIndex > 0 ? this.activeIndex - 1 : this.count - 1;
}
/** Next dot index (cycled) */
get nextIndex() {
return this.activeIndex < this.count - 1 ? this.activeIndex + 1 : 0;
}
/** Returns amount of slides associated with one group(dot) */
get groupSize() {
var _a;
return ((_a = this.$carousel) === null || _a === void 0 ? void 0 : _a.state.count) || 0;
}
/** Current dots collection */
get $dots() {
return [...this.querySelectorAll('[esl-carousel-dot]')];
}
/** @returns ESLCarousel instance; based on {@link carousel} attribute */
get $carousel() {
return ESLTraversingQuery.first(this.carousel, this);
}
/** @returns accessible target ID */
get targetID() {
var _a;
return ((_a = this.$carousel) === null || _a === void 0 ? void 0 : _a.id) || '';
}
connectedCallback() {
super.connectedCallback();
this.replaceChildren();
this.update();
this.updateA11y();
}
disconnectedCallback() {
super.disconnectedCallback();
memoize.clear(this, '$carousel');
}
attributeChangedCallback(name, oldValue, newValue) {
if (!this.connected)
return;
if (name === 'target') {
memoize.clear(this, '$carousel');
this.$$on(this._onSlideChange);
}
this.update(true);
this.updateA11y();
}
/** Updates dots state, rebuilds dots if needed */
update(force) {
memoize.clear(this, ['count', 'activeIndex']); // invalidate state memoization
if (force || this.$dots.length !== this.count) {
const $dots = new Array(this.count).fill(null).map((_, index) => this.dotBuilder(index, this));
// Attribute `esl-carousel-dot` is necessary for proper work of ESLCarouselNavDots plugin, we do not relay on customizable dotBuilder(including default)
$dots.forEach(($dot, index) => $dot.setAttribute('esl-carousel-dot', String(index)));
memoize.clear(this, '$dots');
this.replaceChildren(...$dots);
}
this.$dots.forEach(($dot, index) => this.dotUpdater($dot, index, this));
}
/** Updates a11y of `ESLCarouselNavDots` as a container */
updateA11y() {
this.$$attr('role', 'tablist');
if (!this.hasAttribute('aria-label')) {
this.$$attr('aria-label', ESLCarouselNavDots_1.DEFAULT_ARIA_LABEL);
}
}
/** Handles carousel state changes */
_onSlideChange(e) {
if (this.$carousel === e.target)
this.update();
}
/** Handles `click` on the dots */
_onClick(event) {
if (!this.$carousel || typeof this.$carousel.goTo !== 'function')
return;
const $btn = event.$delegate;
const target = $btn.getAttribute('esl-carousel-dot') || '';
this.$carousel.goTo(`group:${+target}`);
(this.tabIndex >= 0 ? this : $btn).focus({ preventScroll: true });
}
/** Handles `keydown` event */
_onKeydown(event) {
var _a, _b;
if (ARROW_LEFT === event.key) {
(_a = this.$dots[this.prevIndex]) === null || _a === void 0 ? void 0 : _a.click();
}
if (ARROW_RIGHT === event.key) {
(_b = this.$dots[this.nextIndex]) === null || _b === void 0 ? void 0 : _b.click();
}
}
};
ESLCarouselNavDots.is = 'esl-carousel-dots';
ESLCarouselNavDots.observedAttributes = ['target'];
ESLCarouselNavDots.DEFAULT_ARIA_LABEL = 'Carousel dots';
/** Default dots builder function {@link ESLCarouselNavDotBuilder} */
ESLCarouselNavDots.dotBuilder = ESLCarouselNavDots_1.defaultDotBuilder;
/** Default dots updater function {@link ESLCarouselNavDotUpdater} */
ESLCarouselNavDots.dotUpdater = ESLCarouselNavDots_1.defaultDotUpdater;
__decorate([
attr({
name: 'target',
defaultValue: '::parent(.esl-carousel-nav-container)::find(esl-carousel)'
})
], ESLCarouselNavDots.prototype, "carousel", void 0);
__decorate([
attr({ defaultValue: ($this) => `Go to slide ${$this.groupSize > 1 ? 'group ' : ''}{index}` })
], ESLCarouselNavDots.prototype, "dotLabelFormat", void 0);
__decorate([
prop(($this) => $this.constructor.dotBuilder)
], ESLCarouselNavDots.prototype, "dotBuilder", void 0);
__decorate([
prop(($this) => $this.constructor.dotUpdater)
], ESLCarouselNavDots.prototype, "dotUpdater", void 0);
__decorate([
memoize()
], ESLCarouselNavDots.prototype, "count", null);
__decorate([
memoize()
], ESLCarouselNavDots.prototype, "activeIndex", null);
__decorate([
memoize()
], ESLCarouselNavDots.prototype, "$dots", null);
__decorate([
memoize()
], ESLCarouselNavDots.prototype, "$carousel", null);
__decorate([
ready
], ESLCarouselNavDots.prototype, "connectedCallback", null);
__decorate([
listen({
event: `${ESLCarouselSlideEvent.AFTER} ${ESLCarouselChangeEvent.TYPE}`,
target: ($el) => $el.$carousel
})
], ESLCarouselNavDots.prototype, "_onSlideChange", null);
__decorate([
listen({ event: 'click', selector: '[esl-carousel-dot]' })
], ESLCarouselNavDots.prototype, "_onClick", null);
__decorate([
listen('keydown')
], ESLCarouselNavDots.prototype, "_onKeydown", null);
ESLCarouselNavDots = ESLCarouselNavDots_1 = __decorate([
ExportNs('Carousel.Dots')
], ESLCarouselNavDots);
export { ESLCarouselNavDots };