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