UNPKG

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