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 |
|
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);
|
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 | }
|
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);
|
263 |
|
264 | initSlides();
|
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 | }
|
284 |
|
285 |
|
286 | if (hasClickablePagination()) {
|
287 | swiper.pagination.$el.on('keydown', classesToSelector(swiper.params.pagination.bulletClass), onEnterOrSpaceKey);
|
288 | }
|
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 | }
|
316 |
|
317 |
|
318 | if (hasClickablePagination()) {
|
319 | swiper.pagination.$el.off('keydown', classesToSelector(swiper.params.pagination.bulletClass), onEnterOrSpaceKey);
|
320 | }
|
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 |