a11y.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  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. }
  23. });
  24. let liveRegion = null;
  25. function notify(message) {
  26. const notification = liveRegion;
  27. if (notification.length === 0) return;
  28. notification.html('');
  29. notification.html(message);
  30. }
  31. function getRandomNumber(size = 16) {
  32. const randomChar = () => Math.round(16 * Math.random()).toString(16);
  33. return 'x'.repeat(size).replace(/x/g, randomChar);
  34. }
  35. function makeElFocusable($el) {
  36. $el.attr('tabIndex', '0');
  37. }
  38. function makeElNotFocusable($el) {
  39. $el.attr('tabIndex', '-1');
  40. }
  41. function addElRole($el, role) {
  42. $el.attr('role', role);
  43. }
  44. function addElRoleDescription($el, description) {
  45. $el.attr('aria-roledescription', description);
  46. }
  47. function addElControls($el, controls) {
  48. $el.attr('aria-controls', controls);
  49. }
  50. function addElLabel($el, label) {
  51. $el.attr('aria-label', label);
  52. }
  53. function addElId($el, id) {
  54. $el.attr('id', id);
  55. }
  56. function addElLive($el, live) {
  57. $el.attr('aria-live', live);
  58. }
  59. function disableEl($el) {
  60. $el.attr('aria-disabled', true);
  61. }
  62. function enableEl($el) {
  63. $el.attr('aria-disabled', false);
  64. }
  65. function onEnterOrSpaceKey(e) {
  66. if (e.keyCode !== 13 && e.keyCode !== 32) return;
  67. const params = swiper.params.a11y;
  68. const $targetEl = $(e.target);
  69. if (swiper.navigation && swiper.navigation.$nextEl && $targetEl.is(swiper.navigation.$nextEl)) {
  70. if (!(swiper.isEnd && !swiper.params.loop)) {
  71. swiper.slideNext();
  72. }
  73. if (swiper.isEnd) {
  74. notify(params.lastSlideMessage);
  75. } else {
  76. notify(params.nextSlideMessage);
  77. }
  78. }
  79. if (swiper.navigation && swiper.navigation.$prevEl && $targetEl.is(swiper.navigation.$prevEl)) {
  80. if (!(swiper.isBeginning && !swiper.params.loop)) {
  81. swiper.slidePrev();
  82. }
  83. if (swiper.isBeginning) {
  84. notify(params.firstSlideMessage);
  85. } else {
  86. notify(params.prevSlideMessage);
  87. }
  88. }
  89. if (swiper.pagination && $targetEl.is(classesToSelector(swiper.params.pagination.bulletClass))) {
  90. $targetEl[0].click();
  91. }
  92. }
  93. function updateNavigation() {
  94. if (swiper.params.loop || !swiper.navigation) return;
  95. const {
  96. $nextEl,
  97. $prevEl
  98. } = swiper.navigation;
  99. if ($prevEl && $prevEl.length > 0) {
  100. if (swiper.isBeginning) {
  101. disableEl($prevEl);
  102. makeElNotFocusable($prevEl);
  103. } else {
  104. enableEl($prevEl);
  105. makeElFocusable($prevEl);
  106. }
  107. }
  108. if ($nextEl && $nextEl.length > 0) {
  109. if (swiper.isEnd) {
  110. disableEl($nextEl);
  111. makeElNotFocusable($nextEl);
  112. } else {
  113. enableEl($nextEl);
  114. makeElFocusable($nextEl);
  115. }
  116. }
  117. }
  118. function hasPagination() {
  119. return swiper.pagination && swiper.params.pagination.clickable && swiper.pagination.bullets && swiper.pagination.bullets.length;
  120. }
  121. function updatePagination() {
  122. const params = swiper.params.a11y;
  123. if (hasPagination()) {
  124. swiper.pagination.bullets.each(bulletEl => {
  125. const $bulletEl = $(bulletEl);
  126. makeElFocusable($bulletEl);
  127. if (!swiper.params.pagination.renderBullet) {
  128. addElRole($bulletEl, 'button');
  129. addElLabel($bulletEl, params.paginationBulletMessage.replace(/\{\{index\}\}/, $bulletEl.index() + 1));
  130. }
  131. });
  132. }
  133. }
  134. const initNavEl = ($el, wrapperId, message) => {
  135. makeElFocusable($el);
  136. if ($el[0].tagName !== 'BUTTON') {
  137. addElRole($el, 'button');
  138. $el.on('keydown', onEnterOrSpaceKey);
  139. }
  140. addElLabel($el, message);
  141. addElControls($el, wrapperId);
  142. };
  143. function init() {
  144. const params = swiper.params.a11y;
  145. swiper.$el.append(liveRegion); // Container
  146. const $containerEl = swiper.$el;
  147. if (params.containerRoleDescriptionMessage) {
  148. addElRoleDescription($containerEl, params.containerRoleDescriptionMessage);
  149. }
  150. if (params.containerMessage) {
  151. addElLabel($containerEl, params.containerMessage);
  152. } // Wrapper
  153. const $wrapperEl = swiper.$wrapperEl;
  154. const wrapperId = $wrapperEl.attr('id') || `swiper-wrapper-${getRandomNumber(16)}`;
  155. const live = swiper.params.autoplay && swiper.params.autoplay.enabled ? 'off' : 'polite';
  156. addElId($wrapperEl, wrapperId);
  157. addElLive($wrapperEl, live); // Slide
  158. if (params.itemRoleDescriptionMessage) {
  159. addElRoleDescription($(swiper.slides), params.itemRoleDescriptionMessage);
  160. }
  161. addElRole($(swiper.slides), params.slideRole);
  162. const slidesLength = swiper.params.loop ? swiper.slides.filter(el => !el.classList.contains(swiper.params.slideDuplicateClass)).length : swiper.slides.length;
  163. swiper.slides.each((slideEl, index) => {
  164. const $slideEl = $(slideEl);
  165. const slideIndex = swiper.params.loop ? parseInt($slideEl.attr('data-swiper-slide-index'), 10) : index;
  166. const ariaLabelMessage = params.slideLabelMessage.replace(/\{\{index\}\}/, slideIndex + 1).replace(/\{\{slidesLength\}\}/, slidesLength);
  167. addElLabel($slideEl, ariaLabelMessage);
  168. }); // Navigation
  169. let $nextEl;
  170. let $prevEl;
  171. if (swiper.navigation && swiper.navigation.$nextEl) {
  172. $nextEl = swiper.navigation.$nextEl;
  173. }
  174. if (swiper.navigation && swiper.navigation.$prevEl) {
  175. $prevEl = swiper.navigation.$prevEl;
  176. }
  177. if ($nextEl && $nextEl.length) {
  178. initNavEl($nextEl, wrapperId, params.nextSlideMessage);
  179. }
  180. if ($prevEl && $prevEl.length) {
  181. initNavEl($prevEl, wrapperId, params.prevSlideMessage);
  182. } // Pagination
  183. if (hasPagination()) {
  184. swiper.pagination.$el.on('keydown', classesToSelector(swiper.params.pagination.bulletClass), onEnterOrSpaceKey);
  185. }
  186. }
  187. function destroy() {
  188. if (liveRegion && liveRegion.length > 0) liveRegion.remove();
  189. let $nextEl;
  190. let $prevEl;
  191. if (swiper.navigation && swiper.navigation.$nextEl) {
  192. $nextEl = swiper.navigation.$nextEl;
  193. }
  194. if (swiper.navigation && swiper.navigation.$prevEl) {
  195. $prevEl = swiper.navigation.$prevEl;
  196. }
  197. if ($nextEl) {
  198. $nextEl.off('keydown', onEnterOrSpaceKey);
  199. }
  200. if ($prevEl) {
  201. $prevEl.off('keydown', onEnterOrSpaceKey);
  202. } // Pagination
  203. if (hasPagination()) {
  204. swiper.pagination.$el.off('keydown', classesToSelector(swiper.params.pagination.bulletClass), onEnterOrSpaceKey);
  205. }
  206. }
  207. on('beforeInit', () => {
  208. liveRegion = $(`<span class="${swiper.params.a11y.notificationClass}" aria-live="assertive" aria-atomic="true"></span>`);
  209. });
  210. on('afterInit', () => {
  211. if (!swiper.params.a11y.enabled) return;
  212. init();
  213. updateNavigation();
  214. });
  215. on('toEdge', () => {
  216. if (!swiper.params.a11y.enabled) return;
  217. updateNavigation();
  218. });
  219. on('fromEdge', () => {
  220. if (!swiper.params.a11y.enabled) return;
  221. updateNavigation();
  222. });
  223. on('paginationUpdate', () => {
  224. if (!swiper.params.a11y.enabled) return;
  225. updatePagination();
  226. });
  227. on('destroy', () => {
  228. if (!swiper.params.a11y.enabled) return;
  229. destroy();
  230. });
  231. }