123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343 |
- 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();
- });
- }
|