티스토리 뷰

HTML/콤포넌트 모음

웹접근성을 지키는 Swiper

트라이에이스 2021. 11. 25. 15:17

웹접근성을 지키는 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

이슬비님 소스를 수정했습니다.

'HTML > 콤포넌트 모음' 카테고리의 다른 글

인풋 속성 정리  (0) 2023.03.28
원형차트  (0) 2021.12.28
웹접근성 지킨 라디오, 체크박스  (0) 2021.11.24
버튼  (0) 2021.11.22
아코디언  (0) 2021.11.11
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함