<div>
또는 <span>
요소를 사용하여 스크린 리더 사용자에게 오류를 알릴 수 있어야합니다.// ./src/components/signup/SignupForm.tsx
<form className={'form-wrap'} onSubmit={handleFormSubmit}>
<label className={'label'} htmlFor="email">
이메일
</label>
<Input
ref={emailRef}
id="email"
type="email"
error={error === errorDesc[0] || error === errorDesc[1]}
onChange={onChange}
value={inputs.email}
/>
<label className={'label'} htmlFor="password">
비밀번호
</label>
<Input
ref={passwordRef}
id="password"
type="password"
error={error === errorDesc[2]}
onChange={onChange}
value={inputs.password}
/>
<label className={'label'} htmlFor="nickname">
닉네임
</label>
<Input type="text" id="nickname" onChange={onChange} value={inputs.nickname} />
{!isPending && <Button>회원가입</Button>}
{isPending && <strong className={'pending'}>회원가입이 진행중입니다...</strong>}
<div id="error-message" role="alert" aria-live="assertive">
{error && <strong className={'error'}>* {error}</strong>}
</div>
</form>
role="alert"
: 이 속성은 해당 요소의 역할을 정의합니다. "alert" 역할은 중요한 메시지를 나타내며, 이 메시지가 화면에 표시되면 사용자의 주의를 끌어야 함을 나타냅니다.aria-live="assertive"
: 이 속성은 해당 엘리먼트의 내용이 언제 스크린 리더에게 읽혀야 하는지를 나타냅니다. "assertive" 값은 스크린 리더가 가능한 빨리 읽어야 함을 의미합니다.const useSignup = () => {
const [error, setError] = useState<string | null>(null);
const [isPending, setIsPending] = useState(false);
const emailRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
const { dispatch } = useAuthContext();
const signup = (email: string, password: string, nickname: string) => {
setError(null);
setIsPending(true);
createUserWithEmailAndPassword(appAuth, email, password)
.then((userCredential) => {
const user = userCredential.user;
updateProfile(user, { displayName: nickname })
.then(() => {
setError(null);
setIsPending(false);
dispatch({ type: 'LOGIN', payload: user });
})
.catch((err) => {
setError(err.message);
setIsPending(false);
});
})
.catch((err) => {
setError(signupError(err.code));
setIsPending(false);
if (err.code === errorCode[2]) {
passwordRef.current?.focus();
} else {
emailRef.current?.focus();
}
});
};
return { error, isPending, signup, emailRef, passwordRef };
};
export default useSignup;
esc를 누르면 모달창이 닫힙니다. 또한 esc를 눌렀을 대 모달창 외부의 원래 요소에 포커스를 돌려주어, 탐색하던 요소 다음 요소를 계속하여 탐색할 수 있습니다.
모달창 내에서 포커스가 마지막 요소에 도달했을 때, 한 번 더 tab키를 누를 경우 첫 번째 요소로 포커스가 갑니다. 모달창 첫 번째 요소에서 shift + tab키를 누를경우 마지막 요소로 포커스가 갑니다. 모달창 내부에서 포커스가 순환하고 있습니다.
마크업
<div className={styles.overlay}>
<div className={styles.dim} onClick={handleClose}></div>
<article role="dialog" aria-modal="true" aria-labelledby={id} className={styles['modal-container']}>
{children}
<div className={styles['button-group']}>
<Button ref={firstEl} type="button" onClick={handleClose} onKeyDown={handleFirstElKeyDown}>
취소
</Button>
<Button type="button" onClick={handleConfirmClick}>
확인
</Button>
</div>
<button
ref={lastEl}
onKeyDown={handleLastElKeyDown}
className={styles['btn-modal-close']}
type={'button'}
onClick={handleClose}
>
<span className="a11y-hidden">모달 창 닫기 버튼</span>
</button>
</article>
</div>
role="dialog"
: 이 속성은 해당 요소가 모달 다이얼로그임을 나타내는 데 사용됩니다.aria-modal="true"
: 이 속성은 모달 외부로 포커스가 제한되는 것을 명시적으로 전달합니다.aria-labelledby
: 모달의 제목을 스크린 리더 사용자에게 알립니다.키보드 사용자를 위한 코드
// ./src/hooks/useModalKeyEvent.tsx
import { RefObject, useEffect, useRef } from 'react';
import { ButtonKeyboardEvent } from '../typings/eventTypes';
const useModalKeyEvent = (isOpen: boolean, externalBtnRef: RefObject<HTMLButtonElement>, handleClose: () => void) => {
const firstEl = useRef<HTMLButtonElement>(null);
const lastEl = useRef<HTMLButtonElement>(null);
const handleFirstElKeyDown = (e: ButtonKeyboardEvent) => {
if (e.shiftKey && e.key === 'Tab') {
e.preventDefault();
if (lastEl.current) {
lastEl.current.focus();
}
}
};
const handleLastElKeyDown = (e: ButtonKeyboardEvent) => {
if (!e.shiftKey && e.key === 'Tab') {
e.preventDefault();
if (firstEl.current) {
firstEl.current.focus();
}
}
};
useEffect(() => {
const handleEscKeyDown = (e: KeyboardEvent): void => {
if (e.key === 'Escape') {
handleClose();
}
};
if (isOpen) {
document.addEventListener('keydown', handleEscKeyDown);
if (firstEl.current) {
firstEl.current.focus();
}
}
return () => {
document.removeEventListener('keydown', handleEscKeyDown);
if (externalBtnRef.current) {
externalBtnRef.current.focus();
}
};
}, [isOpen]);
return { handleFirstElKeyDown, handleLastElKeyDown, firstEl, lastEl };
};
export default useModalKeyEvent;
handleFirstElKeyDown
: 모달 첫 번째 요소에서 shift + tab키를 눌렀을 때 모달 마지막 요소로 포커스가 가도록 하는 함수입니다.handleLastElKeyDown
: 모달 마지막 요소에서 tab키를 눌렀을 때 모달 첫 번째 요소로 포커스가 가도록 하는 함수입니다.useEffect
내부handleEscKeyDown
: 모달창이 켜진 상태에서 ‘esc’버튼을 눌렀을 때 모달이 꺼지도록 구현하였습니다.if절 내부
: 모달이 켜지면 자동으로 첫번째 요소에 포커스가 가도록 구현하였습니다.