343 lines
11 KiB
JavaScript
343 lines
11 KiB
JavaScript
import classesToSelector from '../../shared/classes-to-selector.js';
|
|
import { createElement, elementIndex } from '../../shared/utils.js';
|
|
export default function 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,
|
|
itemRoleDescriptionMessage: null,
|
|
slideRole: 'group',
|
|
id: null
|
|
}
|
|
});
|
|
swiper.a11y = {
|
|
clicked: false
|
|
};
|
|
let liveRegion = null;
|
|
function notify(message) {
|
|
const notification = liveRegion;
|
|
if (notification.length === 0) return;
|
|
notification.innerHTML = '';
|
|
notification.innerHTML = message;
|
|
}
|
|
const makeElementsArray = el => {
|
|
if (!Array.isArray(el)) el = [el].filter(e => !!e);
|
|
return el;
|
|
};
|
|
function getRandomNumber(size = 16) {
|
|
const randomChar = () => Math.round(16 * Math.random()).toString(16);
|
|
return 'x'.repeat(size).replace(/x/g, randomChar);
|
|
}
|
|
function makeElFocusable(el) {
|
|
el = makeElementsArray(el);
|
|
el.forEach(subEl => {
|
|
subEl.setAttribute('tabIndex', '0');
|
|
});
|
|
}
|
|
function makeElNotFocusable(el) {
|
|
el = makeElementsArray(el);
|
|
el.forEach(subEl => {
|
|
subEl.setAttribute('tabIndex', '-1');
|
|
});
|
|
}
|
|
function addElRole(el, role) {
|
|
el = makeElementsArray(el);
|
|
el.forEach(subEl => {
|
|
subEl.setAttribute('role', role);
|
|
});
|
|
}
|
|
function addElRoleDescription(el, description) {
|
|
el = makeElementsArray(el);
|
|
el.forEach(subEl => {
|
|
subEl.setAttribute('aria-roledescription', description);
|
|
});
|
|
}
|
|
function addElControls(el, controls) {
|
|
el = makeElementsArray(el);
|
|
el.forEach(subEl => {
|
|
subEl.setAttribute('aria-controls', controls);
|
|
});
|
|
}
|
|
function addElLabel(el, label) {
|
|
el = makeElementsArray(el);
|
|
el.forEach(subEl => {
|
|
subEl.setAttribute('aria-label', label);
|
|
});
|
|
}
|
|
function addElId(el, id) {
|
|
el = makeElementsArray(el);
|
|
el.forEach(subEl => {
|
|
subEl.setAttribute('id', id);
|
|
});
|
|
}
|
|
function addElLive(el, live) {
|
|
el = makeElementsArray(el);
|
|
el.forEach(subEl => {
|
|
subEl.setAttribute('aria-live', live);
|
|
});
|
|
}
|
|
function disableEl(el) {
|
|
el = makeElementsArray(el);
|
|
el.forEach(subEl => {
|
|
subEl.setAttribute('aria-disabled', true);
|
|
});
|
|
}
|
|
function enableEl(el) {
|
|
el = makeElementsArray(el);
|
|
el.forEach(subEl => {
|
|
subEl.setAttribute('aria-disabled', false);
|
|
});
|
|
}
|
|
function onEnterOrSpaceKey(e) {
|
|
if (e.keyCode !== 13 && e.keyCode !== 32) return;
|
|
const params = swiper.params.a11y;
|
|
const targetEl = e.target;
|
|
if (swiper.pagination && swiper.pagination.el && (targetEl === swiper.pagination.el || swiper.pagination.el.contains(e.target))) {
|
|
if (!e.target.matches(classesToSelector(swiper.params.pagination.bulletClass))) return;
|
|
}
|
|
if (swiper.navigation && swiper.navigation.nextEl && targetEl === swiper.navigation.nextEl) {
|
|
if (!(swiper.isEnd && !swiper.params.loop)) {
|
|
swiper.slideNext();
|
|
}
|
|
if (swiper.isEnd) {
|
|
notify(params.lastSlideMessage);
|
|
} else {
|
|
notify(params.nextSlideMessage);
|
|
}
|
|
}
|
|
if (swiper.navigation && swiper.navigation.prevEl && targetEl === swiper.navigation.prevEl) {
|
|
if (!(swiper.isBeginning && !swiper.params.loop)) {
|
|
swiper.slidePrev();
|
|
}
|
|
if (swiper.isBeginning) {
|
|
notify(params.firstSlideMessage);
|
|
} else {
|
|
notify(params.prevSlideMessage);
|
|
}
|
|
}
|
|
if (swiper.pagination && targetEl.matches(classesToSelector(swiper.params.pagination.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() {
|
|
return hasPagination() && swiper.params.pagination.clickable;
|
|
}
|
|
function updatePagination() {
|
|
const params = swiper.params.a11y;
|
|
if (!hasPagination()) return;
|
|
swiper.pagination.bullets.forEach(bulletEl => {
|
|
if (swiper.params.pagination.clickable) {
|
|
makeElFocusable(bulletEl);
|
|
if (!swiper.params.pagination.renderBullet) {
|
|
addElRole(bulletEl, 'button');
|
|
addElLabel(bulletEl, params.paginationBulletMessage.replace(/\{\{index\}\}/, elementIndex(bulletEl) + 1));
|
|
}
|
|
}
|
|
if (bulletEl.matches(classesToSelector(swiper.params.pagination.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);
|
|
addElControls(el, wrapperId);
|
|
};
|
|
const handlePointerDown = () => {
|
|
swiper.a11y.clicked = true;
|
|
};
|
|
const handlePointerUp = () => {
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
if (!swiper.destroyed) {
|
|
swiper.a11y.clicked = false;
|
|
}
|
|
});
|
|
});
|
|
};
|
|
const handleFocus = e => {
|
|
if (swiper.a11y.clicked) return;
|
|
const slideEl = e.target.closest(`.${swiper.params.slideClass}, swiper-slide`);
|
|
if (!slideEl || !swiper.slides.includes(slideEl)) return;
|
|
const isActive = swiper.slides.indexOf(slideEl) === swiper.activeIndex;
|
|
const isVisible = swiper.params.watchSlidesProgress && swiper.visibleSlides && swiper.visibleSlides.includes(slideEl);
|
|
if (isActive || isVisible) return;
|
|
if (e.sourceCapabilities && e.sourceCapabilities.firesTouchEvents) return;
|
|
if (swiper.isHorizontal()) {
|
|
swiper.el.scrollLeft = 0;
|
|
} else {
|
|
swiper.el.scrollTop = 0;
|
|
}
|
|
swiper.slideTo(swiper.slides.indexOf(slideEl), 0);
|
|
};
|
|
const initSlides = () => {
|
|
const params = swiper.params.a11y;
|
|
if (params.itemRoleDescriptionMessage) {
|
|
addElRoleDescription(swiper.slides, params.itemRoleDescriptionMessage);
|
|
}
|
|
if (params.slideRole) {
|
|
addElRole(swiper.slides, params.slideRole);
|
|
}
|
|
const slidesLength = swiper.slides.length;
|
|
if (params.slideLabelMessage) {
|
|
swiper.slides.forEach((slideEl, index) => {
|
|
const slideIndex = swiper.params.loop ? parseInt(slideEl.getAttribute('data-swiper-slide-index'), 10) : index;
|
|
const ariaLabelMessage = params.slideLabelMessage.replace(/\{\{index\}\}/, slideIndex + 1).replace(/\{\{slidesLength\}\}/, slidesLength);
|
|
addElLabel(slideEl, ariaLabelMessage);
|
|
});
|
|
}
|
|
};
|
|
const init = () => {
|
|
const params = swiper.params.a11y;
|
|
if (swiper.isElement) {
|
|
swiper.el.shadowEl.append(liveRegion);
|
|
} else {
|
|
swiper.el.append(liveRegion);
|
|
}
|
|
|
|
// Container
|
|
const containerEl = swiper.el;
|
|
if (params.containerRoleDescriptionMessage) {
|
|
addElRoleDescription(containerEl, params.containerRoleDescriptionMessage);
|
|
}
|
|
if (params.containerMessage) {
|
|
addElLabel(containerEl, params.containerMessage);
|
|
}
|
|
|
|
// Wrapper
|
|
const wrapperEl = swiper.wrapperEl;
|
|
const wrapperId = params.id || wrapperEl.getAttribute('id') || `swiper-wrapper-${getRandomNumber(16)}`;
|
|
const live = swiper.params.autoplay && swiper.params.autoplay.enabled ? 'off' : 'polite';
|
|
addElId(wrapperEl, wrapperId);
|
|
addElLive(wrapperEl, live);
|
|
|
|
// Slide
|
|
initSlides();
|
|
|
|
// Navigation
|
|
let {
|
|
nextEl,
|
|
prevEl
|
|
} = swiper.navigation ? swiper.navigation : {};
|
|
nextEl = makeElementsArray(nextEl);
|
|
prevEl = makeElementsArray(prevEl);
|
|
if (nextEl) {
|
|
nextEl.forEach(el => initNavEl(el, wrapperId, params.nextSlideMessage));
|
|
}
|
|
if (prevEl) {
|
|
prevEl.forEach(el => initNavEl(el, wrapperId, params.prevSlideMessage));
|
|
}
|
|
|
|
// Pagination
|
|
if (hasClickablePagination()) {
|
|
const paginationEl = Array.isArray(swiper.pagination.el) ? swiper.pagination.el : [swiper.pagination.el];
|
|
paginationEl.forEach(el => {
|
|
el.addEventListener('keydown', onEnterOrSpaceKey);
|
|
});
|
|
}
|
|
|
|
// Tab focus
|
|
swiper.el.addEventListener('focus', handleFocus, true);
|
|
swiper.el.addEventListener('pointerdown', handlePointerDown, true);
|
|
swiper.el.addEventListener('pointerup', handlePointerUp, true);
|
|
};
|
|
function destroy() {
|
|
if (liveRegion) liveRegion.remove();
|
|
let {
|
|
nextEl,
|
|
prevEl
|
|
} = swiper.navigation ? swiper.navigation : {};
|
|
nextEl = makeElementsArray(nextEl);
|
|
prevEl = makeElementsArray(prevEl);
|
|
if (nextEl) {
|
|
nextEl.forEach(el => el.removeEventListener('keydown', onEnterOrSpaceKey));
|
|
}
|
|
if (prevEl) {
|
|
prevEl.forEach(el => el.removeEventListener('keydown', onEnterOrSpaceKey));
|
|
}
|
|
|
|
// Pagination
|
|
if (hasClickablePagination()) {
|
|
const paginationEl = Array.isArray(swiper.pagination.el) ? swiper.pagination.el : [swiper.pagination.el];
|
|
paginationEl.forEach(el => {
|
|
el.removeEventListener('keydown', onEnterOrSpaceKey);
|
|
});
|
|
}
|
|
|
|
// Tab focus
|
|
swiper.el.removeEventListener('focus', handleFocus, true);
|
|
swiper.el.removeEventListener('pointerdown', handlePointerDown, true);
|
|
swiper.el.removeEventListener('pointerup', handlePointerUp, true);
|
|
}
|
|
on('beforeInit', () => {
|
|
liveRegion = createElement('span', swiper.params.a11y.notificationClass);
|
|
liveRegion.setAttribute('aria-live', 'assertive');
|
|
liveRegion.setAttribute('aria-atomic', 'true');
|
|
});
|
|
on('afterInit', () => {
|
|
if (!swiper.params.a11y.enabled) return;
|
|
init();
|
|
});
|
|
on('slidesLengthChange snapGridLengthChange slidesGridLengthChange', () => {
|
|
if (!swiper.params.a11y.enabled) return;
|
|
initSlides();
|
|
});
|
|
on('fromEdge toEdge afterInit lock unlock', () => {
|
|
if (!swiper.params.a11y.enabled) return;
|
|
updateNavigation();
|
|
});
|
|
on('paginationUpdate', () => {
|
|
if (!swiper.params.a11y.enabled) return;
|
|
updatePagination();
|
|
});
|
|
on('destroy', () => {
|
|
if (!swiper.params.a11y.enabled) return;
|
|
destroy();
|
|
});
|
|
} |