a11y.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. import classesToSelector from '../../shared/classes-to-selector.js';
  2. import { createElement, elementIndex } from '../../shared/utils.js';
  3. export default function A11y({
  4. swiper,
  5. extendParams,
  6. on
  7. }) {
  8. extendParams({
  9. a11y: {
  10. enabled: true,
  11. notificationClass: 'swiper-notification',
  12. prevSlideMessage: 'Previous slide',
  13. nextSlideMessage: 'Next slide',
  14. firstSlideMessage: 'This is the first slide',
  15. lastSlideMessage: 'This is the last slide',
  16. paginationBulletMessage: 'Go to slide {{index}}',
  17. slideLabelMessage: '{{index}} / {{slidesLength}}',
  18. containerMessage: null,
  19. containerRoleDescriptionMessage: null,
  20. itemRoleDescriptionMessage: null,
  21. slideRole: 'group',
  22. id: null
  23. }
  24. });
  25. swiper.a11y = {
  26. clicked: false
  27. };
  28. let liveRegion = null;
  29. function notify(message) {
  30. const notification = liveRegion;
  31. if (notification.length === 0) return;
  32. notification.innerHTML = '';
  33. notification.innerHTML = message;
  34. }
  35. const makeElementsArray = el => {
  36. if (!Array.isArray(el)) el = [el].filter(e => !!e);
  37. return el;
  38. };
  39. function getRandomNumber(size = 16) {
  40. const randomChar = () => Math.round(16 * Math.random()).toString(16);
  41. return 'x'.repeat(size).replace(/x/g, randomChar);
  42. }
  43. function makeElFocusable(el) {
  44. el = makeElementsArray(el);
  45. el.forEach(subEl => {
  46. subEl.setAttribute('tabIndex', '0');
  47. });
  48. }
  49. function makeElNotFocusable(el) {
  50. el = makeElementsArray(el);
  51. el.forEach(subEl => {
  52. subEl.setAttribute('tabIndex', '-1');
  53. });
  54. }
  55. function addElRole(el, role) {
  56. el = makeElementsArray(el);
  57. el.forEach(subEl => {
  58. subEl.setAttribute('role', role);
  59. });
  60. }
  61. function addElRoleDescription(el, description) {
  62. el = makeElementsArray(el);
  63. el.forEach(subEl => {
  64. subEl.setAttribute('aria-roledescription', description);
  65. });
  66. }
  67. function addElControls(el, controls) {
  68. el = makeElementsArray(el);
  69. el.forEach(subEl => {
  70. subEl.setAttribute('aria-controls', controls);
  71. });
  72. }
  73. function addElLabel(el, label) {
  74. el = makeElementsArray(el);
  75. el.forEach(subEl => {
  76. subEl.setAttribute('aria-label', label);
  77. });
  78. }
  79. function addElId(el, id) {
  80. el = makeElementsArray(el);
  81. el.forEach(subEl => {
  82. subEl.setAttribute('id', id);
  83. });
  84. }
  85. function addElLive(el, live) {
  86. el = makeElementsArray(el);
  87. el.forEach(subEl => {
  88. subEl.setAttribute('aria-live', live);
  89. });
  90. }
  91. function disableEl(el) {
  92. el = makeElementsArray(el);
  93. el.forEach(subEl => {
  94. subEl.setAttribute('aria-disabled', true);
  95. });
  96. }
  97. function enableEl(el) {
  98. el = makeElementsArray(el);
  99. el.forEach(subEl => {
  100. subEl.setAttribute('aria-disabled', false);
  101. });
  102. }
  103. function onEnterOrSpaceKey(e) {
  104. if (e.keyCode !== 13 && e.keyCode !== 32) return;
  105. const params = swiper.params.a11y;
  106. const targetEl = e.target;
  107. if (swiper.pagination && swiper.pagination.el && (targetEl === swiper.pagination.el || swiper.pagination.el.contains(e.target))) {
  108. if (!e.target.matches(classesToSelector(swiper.params.pagination.bulletClass))) return;
  109. }
  110. if (swiper.navigation && swiper.navigation.nextEl && targetEl === swiper.navigation.nextEl) {
  111. if (!(swiper.isEnd && !swiper.params.loop)) {
  112. swiper.slideNext();
  113. }
  114. if (swiper.isEnd) {
  115. notify(params.lastSlideMessage);
  116. } else {
  117. notify(params.nextSlideMessage);
  118. }
  119. }
  120. if (swiper.navigation && swiper.navigation.prevEl && targetEl === swiper.navigation.prevEl) {
  121. if (!(swiper.isBeginning && !swiper.params.loop)) {
  122. swiper.slidePrev();
  123. }
  124. if (swiper.isBeginning) {
  125. notify(params.firstSlideMessage);
  126. } else {
  127. notify(params.prevSlideMessage);
  128. }
  129. }
  130. if (swiper.pagination && targetEl.matches(classesToSelector(swiper.params.pagination.bulletClass))) {
  131. targetEl.click();
  132. }
  133. }
  134. function updateNavigation() {
  135. if (swiper.params.loop || swiper.params.rewind || !swiper.navigation) return;
  136. const {
  137. nextEl,
  138. prevEl
  139. } = swiper.navigation;
  140. if (prevEl) {
  141. if (swiper.isBeginning) {
  142. disableEl(prevEl);
  143. makeElNotFocusable(prevEl);
  144. } else {
  145. enableEl(prevEl);
  146. makeElFocusable(prevEl);
  147. }
  148. }
  149. if (nextEl) {
  150. if (swiper.isEnd) {
  151. disableEl(nextEl);
  152. makeElNotFocusable(nextEl);
  153. } else {
  154. enableEl(nextEl);
  155. makeElFocusable(nextEl);
  156. }
  157. }
  158. }
  159. function hasPagination() {
  160. return swiper.pagination && swiper.pagination.bullets && swiper.pagination.bullets.length;
  161. }
  162. function hasClickablePagination() {
  163. return hasPagination() && swiper.params.pagination.clickable;
  164. }
  165. function updatePagination() {
  166. const params = swiper.params.a11y;
  167. if (!hasPagination()) return;
  168. swiper.pagination.bullets.forEach(bulletEl => {
  169. if (swiper.params.pagination.clickable) {
  170. makeElFocusable(bulletEl);
  171. if (!swiper.params.pagination.renderBullet) {
  172. addElRole(bulletEl, 'button');
  173. addElLabel(bulletEl, params.paginationBulletMessage.replace(/\{\{index\}\}/, elementIndex(bulletEl) + 1));
  174. }
  175. }
  176. if (bulletEl.matches(classesToSelector(swiper.params.pagination.bulletActiveClass))) {
  177. bulletEl.setAttribute('aria-current', 'true');
  178. } else {
  179. bulletEl.removeAttribute('aria-current');
  180. }
  181. });
  182. }
  183. const initNavEl = (el, wrapperId, message) => {
  184. makeElFocusable(el);
  185. if (el.tagName !== 'BUTTON') {
  186. addElRole(el, 'button');
  187. el.addEventListener('keydown', onEnterOrSpaceKey);
  188. }
  189. addElLabel(el, message);
  190. addElControls(el, wrapperId);
  191. };
  192. const handlePointerDown = () => {
  193. swiper.a11y.clicked = true;
  194. };
  195. const handlePointerUp = () => {
  196. requestAnimationFrame(() => {
  197. requestAnimationFrame(() => {
  198. if (!swiper.destroyed) {
  199. swiper.a11y.clicked = false;
  200. }
  201. });
  202. });
  203. };
  204. const handleFocus = e => {
  205. if (swiper.a11y.clicked) return;
  206. const slideEl = e.target.closest(`.${swiper.params.slideClass}, swiper-slide`);
  207. if (!slideEl || !swiper.slides.includes(slideEl)) return;
  208. const isActive = swiper.slides.indexOf(slideEl) === swiper.activeIndex;
  209. const isVisible = swiper.params.watchSlidesProgress && swiper.visibleSlides && swiper.visibleSlides.includes(slideEl);
  210. if (isActive || isVisible) return;
  211. if (e.sourceCapabilities && e.sourceCapabilities.firesTouchEvents) return;
  212. if (swiper.isHorizontal()) {
  213. swiper.el.scrollLeft = 0;
  214. } else {
  215. swiper.el.scrollTop = 0;
  216. }
  217. swiper.slideTo(swiper.slides.indexOf(slideEl), 0);
  218. };
  219. const initSlides = () => {
  220. const params = swiper.params.a11y;
  221. if (params.itemRoleDescriptionMessage) {
  222. addElRoleDescription(swiper.slides, params.itemRoleDescriptionMessage);
  223. }
  224. if (params.slideRole) {
  225. addElRole(swiper.slides, params.slideRole);
  226. }
  227. const slidesLength = swiper.slides.length;
  228. if (params.slideLabelMessage) {
  229. swiper.slides.forEach((slideEl, index) => {
  230. const slideIndex = swiper.params.loop ? parseInt(slideEl.getAttribute('data-swiper-slide-index'), 10) : index;
  231. const ariaLabelMessage = params.slideLabelMessage.replace(/\{\{index\}\}/, slideIndex + 1).replace(/\{\{slidesLength\}\}/, slidesLength);
  232. addElLabel(slideEl, ariaLabelMessage);
  233. });
  234. }
  235. };
  236. const init = () => {
  237. const params = swiper.params.a11y;
  238. if (swiper.isElement) {
  239. swiper.el.shadowEl.append(liveRegion);
  240. } else {
  241. swiper.el.append(liveRegion);
  242. }
  243. // Container
  244. const containerEl = swiper.el;
  245. if (params.containerRoleDescriptionMessage) {
  246. addElRoleDescription(containerEl, params.containerRoleDescriptionMessage);
  247. }
  248. if (params.containerMessage) {
  249. addElLabel(containerEl, params.containerMessage);
  250. }
  251. // Wrapper
  252. const wrapperEl = swiper.wrapperEl;
  253. const wrapperId = params.id || wrapperEl.getAttribute('id') || `swiper-wrapper-${getRandomNumber(16)}`;
  254. const live = swiper.params.autoplay && swiper.params.autoplay.enabled ? 'off' : 'polite';
  255. addElId(wrapperEl, wrapperId);
  256. addElLive(wrapperEl, live);
  257. // Slide
  258. initSlides();
  259. // Navigation
  260. let {
  261. nextEl,
  262. prevEl
  263. } = swiper.navigation ? swiper.navigation : {};
  264. nextEl = makeElementsArray(nextEl);
  265. prevEl = makeElementsArray(prevEl);
  266. if (nextEl) {
  267. nextEl.forEach(el => initNavEl(el, wrapperId, params.nextSlideMessage));
  268. }
  269. if (prevEl) {
  270. prevEl.forEach(el => initNavEl(el, wrapperId, params.prevSlideMessage));
  271. }
  272. // Pagination
  273. if (hasClickablePagination()) {
  274. const paginationEl = Array.isArray(swiper.pagination.el) ? swiper.pagination.el : [swiper.pagination.el];
  275. paginationEl.forEach(el => {
  276. el.addEventListener('keydown', onEnterOrSpaceKey);
  277. });
  278. }
  279. // Tab focus
  280. swiper.el.addEventListener('focus', handleFocus, true);
  281. swiper.el.addEventListener('pointerdown', handlePointerDown, true);
  282. swiper.el.addEventListener('pointerup', handlePointerUp, true);
  283. };
  284. function destroy() {
  285. if (liveRegion) liveRegion.remove();
  286. let {
  287. nextEl,
  288. prevEl
  289. } = swiper.navigation ? swiper.navigation : {};
  290. nextEl = makeElementsArray(nextEl);
  291. prevEl = makeElementsArray(prevEl);
  292. if (nextEl) {
  293. nextEl.forEach(el => el.removeEventListener('keydown', onEnterOrSpaceKey));
  294. }
  295. if (prevEl) {
  296. prevEl.forEach(el => el.removeEventListener('keydown', onEnterOrSpaceKey));
  297. }
  298. // Pagination
  299. if (hasClickablePagination()) {
  300. const paginationEl = Array.isArray(swiper.pagination.el) ? swiper.pagination.el : [swiper.pagination.el];
  301. paginationEl.forEach(el => {
  302. el.removeEventListener('keydown', onEnterOrSpaceKey);
  303. });
  304. }
  305. // Tab focus
  306. swiper.el.removeEventListener('focus', handleFocus, true);
  307. swiper.el.removeEventListener('pointerdown', handlePointerDown, true);
  308. swiper.el.removeEventListener('pointerup', handlePointerUp, true);
  309. }
  310. on('beforeInit', () => {
  311. liveRegion = createElement('span', swiper.params.a11y.notificationClass);
  312. liveRegion.setAttribute('aria-live', 'assertive');
  313. liveRegion.setAttribute('aria-atomic', 'true');
  314. });
  315. on('afterInit', () => {
  316. if (!swiper.params.a11y.enabled) return;
  317. init();
  318. });
  319. on('slidesLengthChange snapGridLengthChange slidesGridLengthChange', () => {
  320. if (!swiper.params.a11y.enabled) return;
  321. initSlides();
  322. });
  323. on('fromEdge toEdge afterInit lock unlock', () => {
  324. if (!swiper.params.a11y.enabled) return;
  325. updateNavigation();
  326. });
  327. on('paginationUpdate', () => {
  328. if (!swiper.params.a11y.enabled) return;
  329. updatePagination();
  330. });
  331. on('destroy', () => {
  332. if (!swiper.params.a11y.enabled) return;
  333. destroy();
  334. });
  335. }