티스토리 뷰
웹접근성을 지키는 Swiper
IE에서 사용하기 위해 Swiper버전을 4.5.1로 낮춤.
<!DOCTYPE HTML>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>웹접근성을 지키는 Swiper</title>
<meta http-equiv="x-ua-compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<script src="https://code.jquery.com/jquery-latest.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/4.5.1/js/swiper.js"></script>
<script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script>
<link rel="stylesheet" href="https://limjungk.cafe24.com/dist/swiper4.5.1.css">
<style>
/* 기본속성 */
* { margin: 0; padding: 0; font-family: '맑은 고딕'; box-sizing:border-box;}
button { border:0 none; padding:0; margin:0; font-size:100%; color:#000; vertical-align:middle; background:transparent; cursor:pointer;}
body { padding: 20px;}
ul, ol { list-style: none;}
/* 슬라이더 */
.wrap-slide-box { position: relative; width: 100%; max-width: 700px; height: 400px; margin: 0 auto;}
.wrap-slide-box .swiper-container { width: 100%; height: 100%; margin: 0 auto; overflow: hidden;}
.wrap-slide-box .swiper-slide { display: flex; justify-content: center; align-items: center; flex-wrap: nowrap; flex-direction: column;}
.wrap-slide-box .swiper-slide li a { display: block;}
.wrap-slide-box .swiper-slide li a img { width: 100%;}
.wrap-slide-box .swiper-slide.slide01 { background-color: palegoldenrod;}
.wrap-slide-box .swiper-slide.slide02 { background-color: paleturquoise;}
.wrap-slide-box .swiper-slide.slide03 { background-color:palevioletred;}
.wrap-slide-box .wrap-autoplay-control button { display: block; position: relative; width: 20px; height: 20px; border: 0; text-indent: -99999px; background: transparent; cursor: pointer; }
.wrap-slide-box .wrap-autoplay-control button:before { display: block; content: ''; position: absolute; }
.wrap-slide-box .wrap-autoplay-control button[aria-pressed="false"]:before { top: 4px; left: 4px; width: 12px; height: 12px; border-left: 3px solid #000; border-right: 3px solid #000; box-sizing: border-box;}
.wrap-slide-box .wrap-autoplay-control button[aria-pressed="true"]:before { top: 2px; left: 2px; border-top: 8px solid transparent; border-left: 16px solid #000; border-bottom: 8px solid transparent;}
.wrap-slide-box .prop { display: flex; justify-content: center; align-items: center; position: absolute; z-index: 10; bottom: 7px; left: 0; width: 100%;}
.wrap-slide-box .swiper-pagination {display: flex;}
.wrap-slide-box .swiper-pagination span { margin: 0 2px;}
.wrap-slide-box .swiper-pagination-number { margin: 0 10px;}
.invisible { position: absolute; z-index: -1; display: inline-block; overflow: hidden; height: 1px; width: 1px; border: 0; clip: rect(1px, 1px, 1px, 1px); clip-path: inset(50%); word-break: initial; word-wrap: initial;}
</style>
</head>
<body>
<div id="wrap">
<button type="button">포커스 테스트</button>
<h4>웹접근성을 지키는 Swiper</h4>
<div class="wrap-slide-box">
<div class="swiper-button-next"></div>
<div class="swiper-container">
<ul class="swiper-wrapper">
<li class="swiper-slide slide01" tabindex="-1">
<a href="#"><img src="main_notice1.png" alt=""></a>
</li>
<li class="swiper-slide slide02" tabindex="-1">
<a href="#"><img src="main_notice2.png" alt=""></a>
</li>
<li class="swiper-slide slide03" tabindex="-1">
<a href="#"><img src="main_notice3.png" alt=""></a>
</li>
</ul>
</div>
<div class="swiper-button-prev"></div>
<div class="prop">
<div class="swiper-pagination"></div>
<div class="swiper-pagination-number"><em></em>/<span></span></div>
<div class="wrap-autoplay-control">
<button type="button" aria-pressed="false">일시정지</button>
</div>
</div>
</div>
<button type="button">포커스 테스트</button>
</div>
<script>
if (window.NodeList && !NodeList.prototype.forEach) {
NodeList.prototype.forEach = Array.prototype.forEach;
}
function bindEvent(el, eventName, eventHandler) {
if (el.addEventListener){
el.addEventListener(eventName, eventHandler, false);
} else if (el.attachEvent){
el.attachEvent('on'+eventName, eventHandler);
}
}
var thisSlide, // Swiper Slide
focusOut, // 슬라이드 키보드 접근 확인
slideFocus = {}, // 슬라이드 내부 탭 포커스 가능한 요소 저장
swiperWrapper = document.querySelector('.swiper-wrapper'),
slideAll, // 전체 슬라이드 저장
realSlideAll, // loop 모드 일때 복사 된 슬라이드를 제외한 실제 슬라이드 저장
slideLength, // 슬라이드 갯수 - 1
onClickNavigation, // 슬라이드 이전/다음 버튼으로 슬라이드 전환 확인
navigations = {}, // 슬라이드 이전 다음 버튼
prevEnter; // 이전 버튼 키보드 엔터로 접근 확인
var slideKeyDownEvt = function slideKeyDownEvt(e, idx) {
// back tab : 첫 번째 슬라이드 포커스 시
if (e.key == 'Tab' && e.shiftKey && thisSlide.realIndex === 0) {
focusOut = false;
// back tab : 그 외 슬라이드 포커스 시
} else if (e.key == 'Tab' && e.shiftKey && e.target === slideFocus[idx][0]) {
e.preventDefault();
focusOut = true;
realSlideAll[thisSlide.realIndex - 1].setAttribute('tabindex', '0');
thisSlide.slideTo(thisSlide.activeIndex - 1);
removeSlideTabindex();
} else if (e.key == 'Tab' && !e.shiftKey && e.target === slideFocus[idx][slideFocus[idx].length - 1]) {
if (idx >= slideLength) {
// tab : 마지막 슬라이드 내 마지막 요소 포커스 시
focusOut = false;
} else {
// tab : 그 외 슬라이드 내 마지막 요소 포커스 시
e.preventDefault();
if (realSlideAll[thisSlide.realIndex + 1] <= slideLength) realSlideAll[thisSlide.realIndex + 1].setAttribute('tabindex', '0');
focusOut = true;
thisSlide.slideTo(thisSlide.activeIndex + 1);
removeSlideTabindex();
};
};
};
// 슬라이드 내부 클릭 요소 tabindex 값 삭제
var removeSlideTabindex = function removeSlideTabindex() {
slideAll.forEach(function (element, i) {
var focusTarget = Array.prototype.slice.call(element.querySelectorAll('a, button, input, [role="button"], textarea, select, [tabindex="0"]'));
focusTarget.forEach(function (el, idx) {
if (el.closest('.swiper-slide') === slideAll[thisSlide.activeIndex]) el.removeAttribute('tabindex');
});
});
};
var slideFocusAct = function slideFocusAct(e, idx, next) {
if (onClickNavigation) {
if (e.key == 'Enter' && !next) prevEnter = true;
else if (e.key == 'Tab') {
if (idx === 0) {
idx = slideLength;
thisSlide.slideTo(slideLength + 1, 0);
} else if (idx === realSlideAll.length + 1) {
idx = 0;
thisSlide.slideTo(1, 0);
} else {
idx = idx - 1;
}
if (!e.shiftKey && next || prevEnter && !next) {
e.preventDefault();
slideFocus[idx][0].setAttribute('tabindex', '0');
slideFocus[idx][0].focus();
removeSlideTabindex();
onClickNavigation = false;
prevEnter = false;
};
};
};
};
var swiper = new Swiper('.swiper-container', {
slidesPerView: 1,
speed: 300,
centeredSlides : true, // true시에 슬라이드가 가운데로 배치
autoplay: {
delay: 3000,
disableOnInteraction: false,
},
navigation: {
nextEl: '.wrap-slide-box .swiper-button-next',
prevEl: '.wrap-slide-box .swiper-button-prev',
},
pagination: {
el: ".swiper-pagination",
clickable: true,
},
a11y: {
prevSlideMessage: '이전 슬라이드',
nextSlideMessage: '다음 슬라이드',
},
on: {
init: function() {
thisSlide = this;
slideAll = document.querySelectorAll('.swiper-slide');
realSlideAll = document.querySelectorAll('.swiper-slide:not(.swiper-slide-duplicate)');
slideLength = realSlideAll.length - 1;
navigations['prev'] = document.querySelector('.swiper-button-prev');
navigations['next'] = document.querySelector('.swiper-button-next');
var $swiperPaginationNumber = $('.swiper-pagination-number');
$swiperPaginationNumber.find('em').text(this.activeIndex + 1).siblings('span').text(this.slides.length);
autoPlayBtn = document.querySelector('.wrap-autoplay-control > button');
autoPlayBtn.addEventListener('click', function (e) {
autoPlayState = autoPlayBtn.getAttribute('aria-pressed');
if (autoPlayState === 'false') {
autoPlayBtn.innerText='시작';
autoPlayBtn.setAttribute('aria-pressed', 'true');
thisSlide.autoplay.stop();
} else if (autoPlayState === 'true') {
autoPlayBtn.innerText='일시정지';
autoPlayBtn.setAttribute('aria-pressed', 'false');
thisSlide.autoplay.start();
};
});
slideAll.forEach(function (element, i) {
if (element.classList.contains('swiper-slide-duplicate')) {
element.setAttribute('aria-hidden', 'true');
}
slideAll[thisSlide.activeIndex].setAttribute('tabindex', '0');
var focusTarget = Array.prototype.slice.call(element.querySelectorAll('a, button, input, [role="button"], textarea, select, [tabindex="0"]'));
focusTarget.forEach(function (el, idx) {
el.innerHTML += `<span class="invisible">총 ${slideAll.length} 장의 슬라이드 중 ${i+1}번째 슬라이드입니다.</span>`;
if (el.closest('.swiper-slide') !== slideAll[thisSlide.activeIndex]) {
el.setAttribute('tabindex', '-1');
};
});
});
realSlideAll.forEach(function (element, idx) {
slideFocus[idx] = Array.prototype.slice.call(element.querySelectorAll('a, button, input, [role="button"], textarea, select, [tabindex="0"]'));
slideFocus[idx].unshift(element);
slideFocus[idx][0].removeEventListener('keydown', function (e) {
return slideKeyDownEvt(e, idx);
});
slideFocus[idx][0].addEventListener('keydown', function (e) {
swiper.autoplay.stop();
return slideKeyDownEvt(e, idx);
});
});
Object.keys(navigations).forEach(function (navigation) {
bindEvent(navigation, 'keydown', function () {
onClickNavigation = true;
});
});
navigations['next'].removeEventListener('keydown', function (e) {
return slideFocusAct(e, thisSlide.activeIndex, true);
});
navigations['next'].addEventListener('keydown', function (e) {
return slideFocusAct(e, thisSlide.activeIndex, true);
});
navigations['prev'].removeEventListener('keydown', function (e) {
return slideFocusAct(e, thisSlide.activeIndex, false);
});
navigations['prev'].addEventListener('keydown', function (e) {
return slideFocusAct(e, thisSlide.activeIndex, false);
});
},
slideChange: function() {
var $swiperPaginationNumber = $('.swiper-pagination-number');
$swiperPaginationNumber.find('em').text(this.activeIndex + 1).siblings('span').text(this.slides.length);
},
touchMove: function() {
return onClickNavigation = false;
},
slideNextTransitionEnd: function() {
// 키보드 탭 버튼으로 인한 슬라이드 변경 시 동작
if (focusOut) {
slideFocus[this.realIndex][0].focus();
focusOut = false;
};
},
slidePrevTransitionStart: function() {
// 키보드 탭 버튼으로 인한 슬라이드 변경 시 동작
if (focusOut) {
slideFocus[this.realIndex][slideFocus[this.realIndex].length - 1].focus();
focusOut = false;
};
},
},
});
$('.swiper-container').hover(function() {
swiper.autoplay.stop();
}, function() {
swiper.autoplay.start();
});
</script>
</body>
</html>
참고: https://pxd-fed-blog.web.app/a11y-swiper-slide/
웹 접근성을 준수하는 코드 작성하기 #5
Swiper.js 사용 시 웹접근성 준수 하기
pxd-fed-blog.web.app
이슬비님 소스를 수정했습니다.