a11y.mjs 12 KB

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