a11y.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. import classesToSelector from '../../shared/classes-to-selector.js';
  2. import $ from '../../shared/dom.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.html('');
  33. notification.html(message);
  34. }
  35. function getRandomNumber(size = 16) {
  36. const randomChar = () => Math.round(16 * Math.random()).toString(16);
  37. return 'x'.repeat(size).replace(/x/g, randomChar);
  38. }
  39. function makeElFocusable($el) {
  40. $el.attr('tabIndex', '0');
  41. }
  42. function makeElNotFocusable($el) {
  43. $el.attr('tabIndex', '-1');
  44. }
  45. function addElRole($el, role) {
  46. $el.attr('role', role);
  47. }
  48. function addElRoleDescription($el, description) {
  49. $el.attr('aria-roledescription', description);
  50. }
  51. function addElControls($el, controls) {
  52. $el.attr('aria-controls', controls);
  53. }
  54. function addElLabel($el, label) {
  55. $el.attr('aria-label', label);
  56. }
  57. function addElId($el, id) {
  58. $el.attr('id', id);
  59. }
  60. function addElLive($el, live) {
  61. $el.attr('aria-live', live);
  62. }
  63. function disableEl($el) {
  64. $el.attr('aria-disabled', true);
  65. }
  66. function enableEl($el) {
  67. $el.attr('aria-disabled', false);
  68. }
  69. function onEnterOrSpaceKey(e) {
  70. if (e.keyCode !== 13 && e.keyCode !== 32) return;
  71. const params = swiper.params.a11y;
  72. const $targetEl = $(e.target);
  73. if (swiper.navigation && swiper.navigation.$nextEl && $targetEl.is(swiper.navigation.$nextEl)) {
  74. if (!(swiper.isEnd && !swiper.params.loop)) {
  75. swiper.slideNext();
  76. }
  77. if (swiper.isEnd) {
  78. notify(params.lastSlideMessage);
  79. } else {
  80. notify(params.nextSlideMessage);
  81. }
  82. }
  83. if (swiper.navigation && swiper.navigation.$prevEl && $targetEl.is(swiper.navigation.$prevEl)) {
  84. if (!(swiper.isBeginning && !swiper.params.loop)) {
  85. swiper.slidePrev();
  86. }
  87. if (swiper.isBeginning) {
  88. notify(params.firstSlideMessage);
  89. } else {
  90. notify(params.prevSlideMessage);
  91. }
  92. }
  93. if (swiper.pagination && $targetEl.is(classesToSelector(swiper.params.pagination.bulletClass))) {
  94. $targetEl[0].click();
  95. }
  96. }
  97. function updateNavigation() {
  98. if (swiper.params.loop || swiper.params.rewind || !swiper.navigation) return;
  99. const {
  100. $nextEl,
  101. $prevEl
  102. } = swiper.navigation;
  103. if ($prevEl && $prevEl.length > 0) {
  104. if (swiper.isBeginning) {
  105. disableEl($prevEl);
  106. makeElNotFocusable($prevEl);
  107. } else {
  108. enableEl($prevEl);
  109. makeElFocusable($prevEl);
  110. }
  111. }
  112. if ($nextEl && $nextEl.length > 0) {
  113. if (swiper.isEnd) {
  114. disableEl($nextEl);
  115. makeElNotFocusable($nextEl);
  116. } else {
  117. enableEl($nextEl);
  118. makeElFocusable($nextEl);
  119. }
  120. }
  121. }
  122. function hasPagination() {
  123. return swiper.pagination && swiper.pagination.bullets && swiper.pagination.bullets.length;
  124. }
  125. function hasClickablePagination() {
  126. return hasPagination() && swiper.params.pagination.clickable;
  127. }
  128. function updatePagination() {
  129. const params = swiper.params.a11y;
  130. if (!hasPagination()) return;
  131. swiper.pagination.bullets.each(bulletEl => {
  132. const $bulletEl = $(bulletEl);
  133. if (swiper.params.pagination.clickable) {
  134. makeElFocusable($bulletEl);
  135. if (!swiper.params.pagination.renderBullet) {
  136. addElRole($bulletEl, 'button');
  137. addElLabel($bulletEl, params.paginationBulletMessage.replace(/\{\{index\}\}/, $bulletEl.index() + 1));
  138. }
  139. }
  140. if ($bulletEl.is(`.${swiper.params.pagination.bulletActiveClass}`)) {
  141. $bulletEl.attr('aria-current', 'true');
  142. } else {
  143. $bulletEl.removeAttr('aria-current');
  144. }
  145. });
  146. }
  147. const initNavEl = ($el, wrapperId, message) => {
  148. makeElFocusable($el);
  149. if ($el[0].tagName !== 'BUTTON') {
  150. addElRole($el, 'button');
  151. $el.on('keydown', onEnterOrSpaceKey);
  152. }
  153. addElLabel($el, message);
  154. addElControls($el, wrapperId);
  155. };
  156. const handlePointerDown = () => {
  157. swiper.a11y.clicked = true;
  158. };
  159. const handlePointerUp = () => {
  160. requestAnimationFrame(() => {
  161. requestAnimationFrame(() => {
  162. if (!swiper.destroyed) {
  163. swiper.a11y.clicked = false;
  164. }
  165. });
  166. });
  167. };
  168. const handleFocus = e => {
  169. if (swiper.a11y.clicked) return;
  170. const slideEl = e.target.closest(`.${swiper.params.slideClass}`);
  171. if (!slideEl || !swiper.slides.includes(slideEl)) return;
  172. const isActive = swiper.slides.indexOf(slideEl) === swiper.activeIndex;
  173. const isVisible = swiper.params.watchSlidesProgress && swiper.visibleSlides && swiper.visibleSlides.includes(slideEl);
  174. if (isActive || isVisible) return;
  175. if (e.sourceCapabilities && e.sourceCapabilities.firesTouchEvents) return;
  176. if (swiper.isHorizontal()) {
  177. swiper.el.scrollLeft = 0;
  178. } else {
  179. swiper.el.scrollTop = 0;
  180. }
  181. swiper.slideTo(swiper.slides.indexOf(slideEl), 0);
  182. };
  183. const initSlides = () => {
  184. const params = swiper.params.a11y;
  185. if (params.itemRoleDescriptionMessage) {
  186. addElRoleDescription($(swiper.slides), params.itemRoleDescriptionMessage);
  187. }
  188. if (params.slideRole) {
  189. addElRole($(swiper.slides), params.slideRole);
  190. }
  191. const slidesLength = swiper.params.loop ? swiper.slides.filter(el => !el.classList.contains(swiper.params.slideDuplicateClass)).length : swiper.slides.length;
  192. if (params.slideLabelMessage) {
  193. swiper.slides.each((slideEl, index) => {
  194. const $slideEl = $(slideEl);
  195. const slideIndex = swiper.params.loop ? parseInt($slideEl.attr('data-swiper-slide-index'), 10) : index;
  196. const ariaLabelMessage = params.slideLabelMessage.replace(/\{\{index\}\}/, slideIndex + 1).replace(/\{\{slidesLength\}\}/, slidesLength);
  197. addElLabel($slideEl, ariaLabelMessage);
  198. });
  199. }
  200. };
  201. const init = () => {
  202. const params = swiper.params.a11y;
  203. swiper.$el.append(liveRegion); // Container
  204. const $containerEl = swiper.$el;
  205. if (params.containerRoleDescriptionMessage) {
  206. addElRoleDescription($containerEl, params.containerRoleDescriptionMessage);
  207. }
  208. if (params.containerMessage) {
  209. addElLabel($containerEl, params.containerMessage);
  210. } // Wrapper
  211. const $wrapperEl = swiper.$wrapperEl;
  212. const wrapperId = params.id || $wrapperEl.attr('id') || `swiper-wrapper-${getRandomNumber(16)}`;
  213. const live = swiper.params.autoplay && swiper.params.autoplay.enabled ? 'off' : 'polite';
  214. addElId($wrapperEl, wrapperId);
  215. addElLive($wrapperEl, live); // Slide
  216. initSlides(); // Navigation
  217. let $nextEl;
  218. let $prevEl;
  219. if (swiper.navigation && swiper.navigation.$nextEl) {
  220. $nextEl = swiper.navigation.$nextEl;
  221. }
  222. if (swiper.navigation && swiper.navigation.$prevEl) {
  223. $prevEl = swiper.navigation.$prevEl;
  224. }
  225. if ($nextEl && $nextEl.length) {
  226. initNavEl($nextEl, wrapperId, params.nextSlideMessage);
  227. }
  228. if ($prevEl && $prevEl.length) {
  229. initNavEl($prevEl, wrapperId, params.prevSlideMessage);
  230. } // Pagination
  231. if (hasClickablePagination()) {
  232. swiper.pagination.$el.on('keydown', classesToSelector(swiper.params.pagination.bulletClass), onEnterOrSpaceKey);
  233. } // Tab focus
  234. swiper.$el.on('focus', handleFocus, true);
  235. swiper.$el.on('pointerdown', handlePointerDown, true);
  236. swiper.$el.on('pointerup', handlePointerUp, true);
  237. };
  238. function destroy() {
  239. if (liveRegion && liveRegion.length > 0) liveRegion.remove();
  240. let $nextEl;
  241. let $prevEl;
  242. if (swiper.navigation && swiper.navigation.$nextEl) {
  243. $nextEl = swiper.navigation.$nextEl;
  244. }
  245. if (swiper.navigation && swiper.navigation.$prevEl) {
  246. $prevEl = swiper.navigation.$prevEl;
  247. }
  248. if ($nextEl) {
  249. $nextEl.off('keydown', onEnterOrSpaceKey);
  250. }
  251. if ($prevEl) {
  252. $prevEl.off('keydown', onEnterOrSpaceKey);
  253. } // Pagination
  254. if (hasClickablePagination()) {
  255. swiper.pagination.$el.off('keydown', classesToSelector(swiper.params.pagination.bulletClass), onEnterOrSpaceKey);
  256. } // Tab focus
  257. swiper.$el.off('focus', handleFocus, true);
  258. swiper.$el.off('pointerdown', handlePointerDown, true);
  259. swiper.$el.off('pointerup', handlePointerUp, true);
  260. }
  261. on('beforeInit', () => {
  262. liveRegion = $(`<span class="${swiper.params.a11y.notificationClass}" aria-live="assertive" aria-atomic="true"></span>`);
  263. });
  264. on('afterInit', () => {
  265. if (!swiper.params.a11y.enabled) return;
  266. init();
  267. });
  268. on('slidesLengthChange snapGridLengthChange slidesGridLengthChange', () => {
  269. if (!swiper.params.a11y.enabled) return;
  270. initSlides();
  271. });
  272. on('fromEdge toEdge afterInit lock unlock', () => {
  273. if (!swiper.params.a11y.enabled) return;
  274. updateNavigation();
  275. });
  276. on('paginationUpdate', () => {
  277. if (!swiper.params.a11y.enabled) return;
  278. updatePagination();
  279. });
  280. on('destroy', () => {
  281. if (!swiper.params.a11y.enabled) return;
  282. destroy();
  283. });
  284. }