swiper
Version:
Most modern mobile touch slider and framework with hardware accelerated transitions
403 lines (400 loc) • 15.1 kB
JavaScript
import { c as classesToSelector } from '../shared/classes-to-selector.mjs';
import { a as createElement, s as makeElementsArray, b as elementIndex, w as setInnerHTML } from '../shared/utils.mjs';
const isVirtualEnabled = (swiper) => !!swiper.virtual && !!swiper.params.virtual?.enabled;
const A11y = ({ swiper, extendParams, on }) => {
extendParams({
a11y: {
enabled: true,
notificationClass: 'swiper-notification',
prevSlideMessage: 'Previous slide',
nextSlideMessage: 'Next slide',
firstSlideMessage: 'This is the first slide',
lastSlideMessage: 'This is the last slide',
paginationBulletMessage: 'Go to slide {{index}}',
slideLabelMessage: '{{index}} / {{slidesLength}}',
containerMessage: null,
containerRoleDescriptionMessage: null,
containerRole: null,
itemRoleDescriptionMessage: null,
slideRole: 'group',
id: null,
scrollOnFocus: true,
wrapperLiveRegion: true,
},
});
swiper.a11y = {
clicked: false,
};
let liveRegion = null;
let preventFocusHandler = false;
let focusTargetSlideEl;
let visibilityChangedTimestamp = new Date().getTime();
function getParams() {
return swiper.params.a11y;
}
function notify(message) {
const notification = liveRegion;
if (!notification || !message)
return;
setInnerHTML(notification, message);
}
function getRandomNumber(size = 16) {
const randomChar = () => Math.round(16 * Math.random()).toString(16);
return 'x'.repeat(size).replace(/x/g, randomChar);
}
function makeElFocusable(el) {
const els = makeElementsArray(el);
els.forEach((subEl) => {
subEl.setAttribute('tabIndex', '0');
});
}
function makeElNotFocusable(el) {
const els = makeElementsArray(el);
els.forEach((subEl) => {
subEl.setAttribute('tabIndex', '-1');
});
}
function addElRole(el, role) {
const els = makeElementsArray(el);
els.forEach((subEl) => {
subEl.setAttribute('role', role);
});
}
function addElRoleDescription(el, description) {
const els = makeElementsArray(el);
els.forEach((subEl) => {
subEl.setAttribute('aria-roledescription', description);
});
}
function addElLabel(el, label) {
const els = makeElementsArray(el);
els.forEach((subEl) => {
subEl.setAttribute('aria-label', label);
});
}
function addElId(el, id) {
const els = makeElementsArray(el);
els.forEach((subEl) => {
subEl.setAttribute('id', id);
});
}
function addElLive(el, live) {
const els = makeElementsArray(el);
els.forEach((subEl) => {
subEl.setAttribute('aria-live', live);
});
}
function disableEl(el) {
const els = makeElementsArray(el);
els.forEach((subEl) => {
subEl.setAttribute('aria-disabled', 'true');
});
}
function enableEl(el) {
const els = makeElementsArray(el);
els.forEach((subEl) => {
subEl.removeAttribute('aria-disabled');
});
}
function onEnterOrSpaceKey(e) {
if (e.keyCode !== 13 && e.keyCode !== 32)
return;
const params = getParams();
const paginationParams = swiper.params.pagination;
const targetEl = e.target;
if (swiper.pagination &&
swiper.pagination.el &&
(targetEl === swiper.pagination.el || swiper.pagination.el.contains(targetEl))) {
if (!targetEl.matches(classesToSelector(paginationParams?.bulletClass)))
return;
}
if (swiper.navigation && swiper.navigation.prevEl && swiper.navigation.nextEl) {
const prevEls = makeElementsArray(swiper.navigation.prevEl);
const nextEls = makeElementsArray(swiper.navigation.nextEl);
if (nextEls.includes(targetEl)) {
if (!(swiper.isEnd && !swiper.params.loop)) {
swiper.slideNext();
}
if (swiper.isEnd) {
notify(params.lastSlideMessage);
}
else {
notify(params.nextSlideMessage);
}
}
if (prevEls.includes(targetEl)) {
if (!(swiper.isBeginning && !swiper.params.loop)) {
swiper.slidePrev();
}
if (swiper.isBeginning) {
notify(params.firstSlideMessage);
}
else {
notify(params.prevSlideMessage);
}
}
}
if (swiper.pagination && targetEl.matches(classesToSelector(paginationParams?.bulletClass))) {
targetEl.click();
}
}
function updateNavigation() {
if (swiper.params.loop || swiper.params.rewind || !swiper.navigation)
return;
const { nextEl, prevEl } = swiper.navigation;
if (prevEl) {
if (swiper.isBeginning) {
disableEl(prevEl);
makeElNotFocusable(prevEl);
}
else {
enableEl(prevEl);
makeElFocusable(prevEl);
}
}
if (nextEl) {
if (swiper.isEnd) {
disableEl(nextEl);
makeElNotFocusable(nextEl);
}
else {
enableEl(nextEl);
makeElFocusable(nextEl);
}
}
}
function hasPagination() {
return !!(swiper.pagination && swiper.pagination.bullets && swiper.pagination.bullets.length);
}
function hasClickablePagination() {
const paginationParams = swiper.params.pagination;
return hasPagination() && !!paginationParams?.clickable;
}
function updatePagination() {
const params = getParams();
if (!hasPagination())
return;
const paginationParams = swiper.params.pagination;
swiper.pagination.bullets.forEach((bulletEl) => {
if (paginationParams.clickable) {
makeElFocusable(bulletEl);
if (!paginationParams.renderBullet) {
addElRole(bulletEl, 'button');
addElLabel(bulletEl, params.paginationBulletMessage.replace(/\{\{index\}\}/, String((elementIndex(bulletEl) ?? 0) + 1)));
}
}
if (bulletEl.matches(classesToSelector(paginationParams.bulletActiveClass))) {
bulletEl.setAttribute('aria-current', 'true');
}
else {
bulletEl.removeAttribute('aria-current');
}
});
}
const initNavEl = (el, _wrapperId, message) => {
makeElFocusable(el);
if (el.tagName !== 'BUTTON') {
addElRole(el, 'button');
el.addEventListener('keydown', onEnterOrSpaceKey);
}
addElLabel(el, message);
};
const handlePointerDown = (e) => {
if (focusTargetSlideEl &&
focusTargetSlideEl !== e.target &&
!focusTargetSlideEl.contains(e.target)) {
preventFocusHandler = true;
}
swiper.a11y.clicked = true;
};
const handlePointerUp = () => {
preventFocusHandler = false;
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (!swiper.destroyed) {
swiper.a11y.clicked = false;
}
});
});
};
const onVisibilityChange = (_e) => {
visibilityChangedTimestamp = new Date().getTime();
};
const handleFocus = (e) => {
const params = getParams();
if (swiper.a11y.clicked || !params.scrollOnFocus)
return;
if (new Date().getTime() - visibilityChangedTimestamp < 100)
return;
const target = e.target;
const slideEl = target.closest(`.${swiper.params.slideClass}, swiper-slide`);
if (!slideEl || !swiper.slides.includes(slideEl))
return;
focusTargetSlideEl = slideEl;
const isVirtual = isVirtualEnabled(swiper);
const isActive = (isVirtual
? parseInt(slideEl.getAttribute('data-swiper-slide-index') || '0', 10)
: swiper.slides.indexOf(slideEl)) === swiper.activeIndex;
const isVisible = swiper.params.watchSlidesProgress &&
swiper.visibleSlides &&
swiper.visibleSlides.includes(slideEl);
if (isActive || isVisible)
return;
const sourceCapabilities = e.sourceCapabilities;
if (sourceCapabilities && sourceCapabilities.firesTouchEvents)
return;
if (swiper.isHorizontal()) {
swiper.el.scrollLeft = 0;
}
else {
swiper.el.scrollTop = 0;
}
requestAnimationFrame(() => {
if (preventFocusHandler)
return;
if (swiper.params.loop) {
swiper.slideToLoop(swiper.getSlideIndexWhenGrid(parseInt(slideEl.getAttribute('data-swiper-slide-index') || '0', 10)), 0);
}
else if (isVirtual) {
swiper.slideTo(swiper.getSlideIndexWhenGrid(parseInt(slideEl.getAttribute('data-swiper-slide-index') || '0', 10)), 0);
}
else {
swiper.slideTo(swiper.getSlideIndexWhenGrid(swiper.slides.indexOf(slideEl)), 0);
}
preventFocusHandler = false;
});
};
const initSlides = () => {
const params = getParams();
if (params.itemRoleDescriptionMessage) {
addElRoleDescription(swiper.slides, params.itemRoleDescriptionMessage);
}
if (params.slideRole) {
addElRole(swiper.slides, params.slideRole);
}
const slidesLength = swiper.slides.length;
const slideLabelMessage = params.slideLabelMessage;
if (slideLabelMessage) {
swiper.slides.forEach((slideEl, index) => {
const slideIndex = swiper.params.loop
? parseInt(slideEl.getAttribute('data-swiper-slide-index') || '0', 10)
: index;
const ariaLabelMessage = slideLabelMessage
.replace(/\{\{index\}\}/, String(slideIndex + 1))
.replace(/\{\{slidesLength\}\}/, String(slidesLength));
addElLabel(slideEl, ariaLabelMessage);
});
}
};
const init = () => {
const params = getParams();
if (liveRegion)
swiper.el.append(liveRegion);
// Container
const containerEl = swiper.el;
if (params.containerRoleDescriptionMessage) {
addElRoleDescription(containerEl, params.containerRoleDescriptionMessage);
}
if (params.containerMessage) {
addElLabel(containerEl, params.containerMessage);
}
if (params.containerRole) {
addElRole(containerEl, params.containerRole);
}
// Wrapper
const wrapperEl = swiper.wrapperEl;
const wrapperId = String(params.id || wrapperEl.getAttribute('id') || `swiper-wrapper-${getRandomNumber(16)}`);
addElId(wrapperEl, wrapperId);
if (params.wrapperLiveRegion) {
const autoplayParams = swiper.params.autoplay;
const live = swiper.params.autoplay && autoplayParams?.enabled ? 'off' : 'polite';
addElLive(wrapperEl, live);
}
// Slide
initSlides();
// Navigation
const nav = swiper.navigation
? swiper.navigation
: { nextEl: undefined, prevEl: undefined };
const nextEls = makeElementsArray(nav.nextEl);
const prevEls = makeElementsArray(nav.prevEl);
if (nextEls) {
nextEls.forEach((el) => initNavEl(el, wrapperId, params.nextSlideMessage));
}
if (prevEls) {
prevEls.forEach((el) => initNavEl(el, wrapperId, params.prevSlideMessage));
}
// Pagination
if (hasClickablePagination()) {
const paginationEl = makeElementsArray(swiper.pagination.el);
paginationEl.forEach((el) => {
el.addEventListener('keydown', onEnterOrSpaceKey);
});
}
// Tab focus
document.addEventListener('visibilitychange', onVisibilityChange);
swiper.el.addEventListener('focus', handleFocus, true);
swiper.el.addEventListener('pointerdown', handlePointerDown, true);
swiper.el.addEventListener('pointerup', handlePointerUp, true);
};
function destroy() {
if (liveRegion)
liveRegion.remove();
const nav = swiper.navigation
? swiper.navigation
: { nextEl: undefined, prevEl: undefined };
const nextEls = makeElementsArray(nav.nextEl);
const prevEls = makeElementsArray(nav.prevEl);
if (nextEls) {
nextEls.forEach((el) => el.removeEventListener('keydown', onEnterOrSpaceKey));
}
if (prevEls) {
prevEls.forEach((el) => el.removeEventListener('keydown', onEnterOrSpaceKey));
}
// Pagination
if (hasClickablePagination()) {
const paginationEl = makeElementsArray(swiper.pagination.el);
paginationEl.forEach((el) => {
el.removeEventListener('keydown', onEnterOrSpaceKey);
});
}
document.removeEventListener('visibilitychange', onVisibilityChange);
// Tab focus
if (swiper.el && typeof swiper.el !== 'string') {
swiper.el.removeEventListener('focus', handleFocus, true);
swiper.el.removeEventListener('pointerdown', handlePointerDown, true);
swiper.el.removeEventListener('pointerup', handlePointerUp, true);
}
}
on('beforeInit', () => {
liveRegion = createElement('span', getParams().notificationClass);
liveRegion.setAttribute('aria-live', 'assertive');
liveRegion.setAttribute('aria-atomic', 'true');
});
on('afterInit', () => {
if (!getParams().enabled)
return;
init();
});
on('slidesLengthChange snapGridLengthChange slidesGridLengthChange', () => {
if (!getParams().enabled)
return;
initSlides();
});
on('fromEdge toEdge afterInit lock unlock', () => {
if (!getParams().enabled)
return;
updateNavigation();
});
on('paginationUpdate', () => {
if (!getParams().enabled)
return;
updatePagination();
});
on('destroy', () => {
if (!getParams().enabled)
return;
destroy();
});
};
export { A11y as default };