[HTML-Javascript] 초점 반환, 두 줄씩만 넣어도 간편해집니다.
브라우저에서 제공하는 UI의 확장성에 불만이 많은 웹 개발자 및 디자이너는 메뉴나 콤보박스, 대화상자를 따로 만들기 시작했고, 오늘 날에는 형형색색 다양한 UI가 존재합니다. 이로 인해 생기는 문제점은 당연히 '웹 표준'이나 '접근성'이 취약하다는 점입니다.
특히, 대화상자는 초점을 관리해주지 않아서 서비스를 이용하는 데, 큰 어려움이 있어왔고, 여전히 ~ing입니다. 접근성을 조금이라도 신경쓰는 기업 또는 개발자는 적어도, 대화상자가 나타났을 때, 초점을 대화상자 안으로 보내주는 노력이 이어지고는 있지만, 여전히 대체로 구현되지 않습니다.
평상시에는 대화상자가 열렸을 때 초점 관리에 관해 다뤘다면, 오늘은 대화상자가 닫혔을 때 초점에 관해 팁을 드리고자 합니다. "보내주기만 하면 됐지, 뭘 또 하라는거냐"라고 생각하실 수 있고, 바쁜 거 압니다. 하지만, 코드로 봤을 때, 메소드에 한 두줄씩만 더 추가해서 더 좋은 결과를 낼 수 있습니다.
대화상자를 닫았을 때, 마우스 사용자는 대화상자 밖에 있는 어떤 요소든 다시 클릭하는 데, 오랜 시간이 걸리지 않습니다. 보이는 스크롤 위치부터 보고, 포인터를 움직여서 누르기만 하면 되니까요.
키보드 사용자는 어떨까요? 키보드 사용자는, 개발자의 설계 의도나 특별한 소프트웨어가 없다면 링크나 버튼 등을 모두 순서대로 하나씩 전부 탐색하게 됩니다. 만약에, 초점을 대화상자가 연 요소로 이동해주지 않고 대화상자만 닫힌다면 어떨까요? 문서에서 초점을 잃어버리기 때문에 처음부터 다시 탐색해야 합니다.
즉, 탐색과 조작의 연속성이 매우 떨어지게 됩니다. 이것을 "초점 반환"이라고 표현하겠습니다. 초점반환은 지난 아티클인 '조작의 연속성'과 아주 밀접한 연관이 있습니다.
지금부터 당신은 컴퓨터 사용이 서툰 사람이라고 가정해봅시다. 만약에, 회원가입 페이지가 있는데, 회원가입을 완료했더니, "가입하신걸 진심으로 환영합니다!"라는 페이지로 리디렉팅 된다고 생각해봅시다. 그런데, 리디렉팅된 페이지에 "로그인"이나 "홈으로 이동"과 같은 링크가 없다면 어떨까요?
매우 불편할 것입니다. 홈이나 로그인 url을 알고 있어서, 다시 접속해야겠지요. 컴퓨터 사용이 미숙한 분이라면 아마 매우 큰 수고가 필요할 겁니다. 이런 관점에서 이 초점 반환은 대화상자나, 메뉴, 콤보박스를 구현할 때, 반드시 필요한 과정입니다.
class CustomModalDialog extends HTMLElement {
#returnFocus;
set opened(v) { this.toggleAttribute('open',v); }
get opened() { return this.hasAttribute("open"); }
open ( ) {
this.#returnFocus = document.activeElement;
document.body.querySelectorAll(":scope>*:not(.modal-wrapper)").forEach(_=>{
_.setAttribute('inert',"");
});
this.opened = true;
this.focus();
}
close ( ) {
this.opened = false;
document.body.querySelectorAll(":scope>*:not(.modal-wrapper)").forEach(_=>{
_.removeAttribute('inert',"");
});
this.returnFocus.focus();
}
set returnFocus (element) { this.#returnFocus = element; }
get returnFocus () { return this.#returnFocus; }
constructor() {
super();
this.attachShadow({mode:"open"});
this.opened = false;
this.role = "dialog";
this.tabIndex = -1;
this.ariaModal = true;
const templateHTML = `<style>
:host {
top:0; left:0;
width:100%; height:100%;
display:flex;
position:fixed;
z-index:997;
font-size:1.45rem;
}
:host(:not([open])) {
display:none;
}
#backdrop {
display:flex;
width:100%;
height:100%;
background:rgba(0,0,0,0.5);
}
#body {
display:flex;
flex-direction:column;
background-color:white;
min-width:50%; max-width:80%; border-radius:0.5rem;
aspect-ratio:16/9;
overflow:hidden;
margin: auto;
padding:2rem;
}
</style>
<div id="backdrop">
<div id="body">
<slot></slot>
</div>
</div>`;
const templateElement = document.createElement("template");
templateElement.innerHTML = templateHTML;
this.shadowRoot.append(templateElement.content.cloneNode(true));
}
connectedCallback() {
this.shadowRoot.querySelector("#backdrop").addEventListener("click",e=>{
if(e.target == this.shadowRoot.querySelector("#backdrop")) this.close();
});
this.addEventListener("keydown",e=>{
if(e.key == "Escape") this.close();
});
this.querySelectorAll(".btn-close").forEach(btn=>{
btn.addEventListener("click",()=>{
this.close();
})
});
if(
!this.parentElement.classList.contains('modal-wrapper')
||
![...document.body.childNodes].find(_=>_=== this.parentElement)
) {
console.error("[Error] modal-dialog 태그는 body>.modal-wrapper 컨테이너 안에 배치해야 합니다.")
this.parentElement.removeChild(this);
delete this;
}
}
}
customElements.define("modal-dialog",CustomModalDialog);
위 코드를 한번 보세요. 이것은 customElements API로 만든 대화상자 컴포넌트입니다. 그리 긴 코드가 아닙니다. 이 컴포넌트의 open 메소드를 보시면 this.returnElement에 document.activeElement를 저장하는 것을 볼 수 있습니다. 이 녀석이 뭐길레 저장할까요?
현재 키보드 초점을 가리키는 속성: document.activeElement
알 사람도 알겠지만, 웹 표준 DOM의 document 객체에는 activeElement라는 녀석이 있습니다. 현재 키보드 초점이 있는 요소 위치를 감지할 수 있는 속성입니다. 대화상자가 열 때, 특정 변수나 속성에 이 activeElement를 담으면, 그 시점에 초점을 가지고 있는 요소가 담기게 됩니다.
이렇게 담겨진 요소는 나중에 close 메소드가 여러 이벤트에서 실행될 때, 초점을 다시 대화상자가 열리기 전 요소로 반환하는 용도로 사용할 수 있습니다. 특히, Windows 환경에서 이 document.activeElement는 유용합니다.
모바일은 어쩌고요?
모바일은 안타깝게도 TalkBack과 호환성이 맞지 않습니다. iOS만 놓고 보면, VoiceOver 커서는 마치 키보드의 Tab 키를 눌러서 이동하는 것처럼 링크나 버튼, 편집창에 커서가 있으면, 시스템 초점도 따라가게 됩니다. 그런데, TalkBack의 터치 커서는 시스템 커서에 관여하지 않기 때문에 한계가 있습니다.
그렇기 때문에, 일반적으로 모바일까지 고려한다면, aria-haspopup="dialog", aria-controls를 사용하여 둘을 연결하는 것을 권장합니다. 다만, 단순히 PC 페이지에서 사용될 대화상자라면, 이 document.activeElement만으로 초점을 쉽게 반환할 수 있습니다.