Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] 스크린 리더기 focus 개선 #347

Merged
merged 12 commits into from
Oct 21, 2024
Merged
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';
import { useRecoilState } from 'recoil';

import {
Expand All @@ -25,9 +25,10 @@ const ReadyMembersContainer = () => {
const { members, master } = useGetRoomInfo();
const { show } = useModal();
const [memberInfo, setMemberInfo] = useRecoilState(memberInfoState);
const closeRef = useRef<HTMLButtonElement>(null);

const handleClickInvite = () => {
show(InviteModal);
show(InviteModal, { closeRef });
};

// 원래 방장이 아니다 + 방장의 memberId와 내 memberId가 같다 -> 방장으로 변경
Expand All @@ -42,7 +43,7 @@ const ReadyMembersContainer = () => {
<A11yOnly aria-live="polite">총 인원 {members.length}명</A11yOnly>
<div css={totalNumber}>
<div aria-hidden>총 인원 {members.length}명</div>
<button css={inviteButton} onClick={handleClickInvite}>
<button css={inviteButton} onClick={handleClickInvite} ref={closeRef}>
초대하기
</button>
</div>
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/RoomSetting/RoomSetting.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useRef } from 'react';
import { useRecoilValue } from 'recoil';

import {
Expand All @@ -15,6 +16,7 @@ import useModal from '@/hooks/useModal';
import { memberInfoState } from '@/recoil/atom';

const RoomSetting = () => {
const closeRef = useRef<HTMLButtonElement>(null);
const { roomSetting } = useGetRoomInfo();
const { isMaster } = useRecoilValue(memberInfoState);
const { show } = useModal();
Expand All @@ -25,7 +27,7 @@ const RoomSetting = () => {
타이머 ${roomSetting.timeLimit / 1000}초.`;

const handleClickCategory = () => {
show(RoomSettingModal);
show(RoomSettingModal, { closeRef });
};

return (
Expand All @@ -35,6 +37,7 @@ const RoomSetting = () => {
aria-label="방 설정"
css={roomSettingLayout}
onClick={isMaster ? handleClickCategory : () => {}}
ref={closeRef}
>
<div css={roomSettingBox}>
<span css={roomSettingLabel}>라운드</span>
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/components/common/AlertModal/AlertModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Fragment } from 'react';
import { Fragment, RefObject } from 'react';

import { alertModalTitle, alertText, messageContainer } from './AlertModal.styled';
import Modal from '../Modal/Modal';
Expand All @@ -9,16 +9,17 @@ interface AlertModalProps {
onConfirm?: () => void;
message?: string;
title?: string;
closeRef?: RefObject<HTMLElement>;
}

const AlertModal = ({ isOpen, onClose, onConfirm, message, title }: AlertModalProps) => {
const AlertModal = ({ isOpen, onClose, onConfirm, message, title, closeRef }: AlertModalProps) => {
const handleClick = () => {
onConfirm && onConfirm();
onClose();
};

return (
<Modal isOpen={isOpen} onClose={onClose}>
<Modal isOpen={isOpen} onClose={onClose} closeRef={closeRef}>
<Modal.Header position="center">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 질문 💭

Button에는 ref로 적용했는데, Modal에서는 forwardRef를 적용하지 않은 이유가 궁금합니다!

<Modal.Title css={alertModalTitle}>{title || '알림'}</Modal.Title>
<Modal.IconButton onClick={onClose} />
Expand Down
40 changes: 18 additions & 22 deletions frontend/src/components/common/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ButtonHTMLAttributes } from 'react';
import React, { ButtonHTMLAttributes, forwardRef } from 'react';

import { buttonLayout } from './Button.styled';

Expand All @@ -13,26 +13,22 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
bottom?: boolean;
}

const Button: React.FC<ButtonProps> = ({
text,
onClick,
disabled,
size,
radius,
fontSize,
bottom,
...props
}) => {
return (
<button
onClick={onClick}
disabled={disabled}
css={buttonLayout({ disabled, size, radius, fontSize, bottom })}
{...props}
>
{text}
</button>
);
};
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ text, onClick, disabled, size, radius, fontSize, bottom, ...props }, ref) => {
return (
<button
ref={ref}
onClick={onClick}
disabled={disabled}
css={buttonLayout({ disabled, size, radius, fontSize, bottom })}
{...props}
>
{text}
</button>
);
},
);

Button.displayName = 'Button';

export default Button;
6 changes: 4 additions & 2 deletions frontend/src/components/common/InviteModal/InviteModal.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { RefObject } from 'react';
import QRCode from 'react-qr-code';
import { useRecoilValue } from 'recoil';

Expand All @@ -23,9 +24,10 @@ import { roomUuidState } from '@/recoil/atom';
interface InviteModalProps {
isOpen: boolean;
onClose: () => void;
closeRef?: RefObject<HTMLElement>;
}

const InviteModal = ({ isOpen, onClose }: InviteModalProps) => {
const InviteModal = ({ isOpen, onClose, closeRef }: InviteModalProps) => {
const roomUuid = useRecoilValue(roomUuidState);
const inviteUrl = INVITE_URL(roomUuid);

Expand All @@ -38,7 +40,7 @@ const InviteModal = ({ isOpen, onClose }: InviteModalProps) => {
};

return (
<Modal isOpen={isOpen} onClose={onClose} css={inviteModalLayout}>
<Modal isOpen={isOpen} onClose={onClose} css={inviteModalLayout} closeRef={closeRef}>
<Modal.Header position="center">
<Modal.Title css={inviteModalTitle}>초대하기</Modal.Title>
<Modal.IconButton onClick={onClose} />
Expand Down
33 changes: 28 additions & 5 deletions frontend/src/components/common/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { ButtonHTMLAttributes, HTMLAttributes, useRef } from 'react';
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
import React, { ButtonHTMLAttributes, HTMLAttributes, RefObject, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';

import useDisableBackgroundScroll from './hooks/useDisableBackgroundScroll';
import useModalEscClose from './hooks/useModalEscClose';
import {
modalBackdropLayout,
Expand All @@ -17,18 +17,27 @@ import {
} from './Modal.styled';

import CloseIcon from '@/assets/images/closeIcon.png';
import useFocus from '@/hooks/useFocus';

export interface ModalProps
extends React.PropsWithChildren<{
isOpen: boolean;
onClose: () => void;
position?: 'top' | 'bottom' | 'center';
style?: React.CSSProperties;
closeRef?: RefObject<HTMLElement>;
}> {}

const Modal = ({ children, isOpen, onClose, position = 'center', ...restProps }: ModalProps) => {
const Modal = ({
children,
isOpen,
onClose,
closeRef,
position = 'center',
...restProps
}: ModalProps) => {
const modalRef = useRef<HTMLDivElement | null>(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image


const focusRef = useFocus<HTMLDivElement>();
useModalEscClose(isOpen, onClose);

const handleOutsideClick = (event: React.MouseEvent | React.KeyboardEvent) => {
Expand All @@ -37,12 +46,26 @@ const Modal = ({ children, isOpen, onClose, position = 'center', ...restProps }:
}
};

useEffect(() => {
return () => {
if (closeRef?.current) {
closeRef.current.focus();
}
};
}, [closeRef?.current]);

if (!isOpen) return null;

const modalContent = (
/* eslint jsx-a11y/no-static-element-interactions: "off" */
// 모달을 제외한 영역을 클릭시 모달이 꺼지도록 설정하기 위해 설정함
<div css={modalBackdropLayout} onClick={handleOutsideClick} onKeyDown={handleOutsideClick}>
<div
tabIndex={0}
ref={focusRef}
css={modalBackdropLayout}
onClick={handleOutsideClick}
onKeyDown={handleOutsideClick}
>
<div css={modalContentWrapper({ position })} ref={modalRef} {...restProps}>
{children}
</div>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useRef } from 'react';
import { useParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';

Expand All @@ -17,18 +18,20 @@ const NextRoundButton = () => {
const { mutate: moveNextRound } = useMoveNextRoundMutation(Number(roomId));
const memberInfo = useRecoilValue(memberInfoState);
const { show } = useModal();
const closeRef = useRef<HTMLButtonElement>(null);

const randomRoundNextMessage = createRandomNextRoundMessage();
const isLastRound = balanceContent?.currentRound === balanceContent?.totalRound;

const showModal = () => {
show(AlertModal, { message: randomRoundNextMessage, onConfirm: moveNextRound });
show(AlertModal, { message: randomRoundNextMessage, onConfirm: moveNextRound, closeRef });
};

return (
<div css={bottomButtonLayout}>
{memberInfo.isMaster ? (
<Button
ref={closeRef}
style={{ width: '100%' }}
text={isLastRound ? '결과 확인' : '다음'}
onClick={isLastRound ? moveNextRound : showModal}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { RefObject } from 'react';

import CategoryDropdown from './CategoryDropdown/CategoryDropdown';
import useRoomSetting from './hooks/useRoomSetting';
import RoomSettingContainer from './RoomSettingContainer/RoomSettingContainer';
Expand All @@ -17,9 +19,10 @@ const TIMER_PER_ROUND_LIST = [5000, 10000, 15000];
interface RoomSettingModalProps {
isOpen: boolean;
onClose: () => void;
closeRef?: RefObject<HTMLElement>;
}

const RoomSettingModal = ({ isOpen, onClose }: RoomSettingModalProps) => {
const RoomSettingModal = ({ isOpen, onClose, closeRef }: RoomSettingModalProps) => {
const {
roomSetting,
handleClickOption,
Expand All @@ -31,7 +34,7 @@ const RoomSettingModal = ({ isOpen, onClose }: RoomSettingModalProps) => {
const { category, totalRound, timeLimitPerRound } = roomSetting;

return (
<Modal isOpen={isOpen} onClose={onClose} css={roomSettingModalLayout}>
<Modal isOpen={isOpen} onClose={onClose} closeRef={closeRef} css={roomSettingModalLayout}>
<Modal.Header position="center">
<Modal.Title css={roomSettingModalTitle}>방 설정</Modal.Title>
<Modal.IconButton onClick={onClose} />
Expand Down
12 changes: 8 additions & 4 deletions frontend/src/components/layout/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
import { useRef } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';

Expand All @@ -23,6 +25,7 @@ import AlertModal from '@/components/common/AlertModal/AlertModal';
import RoomSettingModal from '@/components/common/RoomSettingModal/RoomSettingModal';
import { convertMsecToSecond } from '@/components/SelectContainer/Timer/Timer.util';
import useBalanceContentQuery from '@/hooks/useBalanceContentQuery';
import useFocus from '@/hooks/useFocus';
import useModal from '@/hooks/useModal';
import { memberInfoState } from '@/recoil/atom';

Expand Down Expand Up @@ -63,9 +66,10 @@ export const RoomSettingHeader = ({ title }: HeaderProps) => {
const { show } = useModal();
const { isMaster } = useRecoilValue(memberInfoState);
const { handleExit } = useExit();
const closeRef = useRef(null);

const handleClickRoomSetting = () => {
show(RoomSettingModal);
show(RoomSettingModal, { closeRef });
};

const handleClickExit = () => {
Expand All @@ -79,7 +83,7 @@ export const RoomSettingHeader = ({ title }: HeaderProps) => {
</button>
<h1 css={gameTitle}>{title}</h1>
{isMaster ? (
<button onClick={handleClickRoomSetting} css={buttonWrapper}>
<button ref={closeRef} onClick={handleClickRoomSetting} css={buttonWrapper}>
<img src={SettingIcon} alt="방 설정" css={iconImage} />
</button>
) : (
Expand Down Expand Up @@ -134,13 +138,13 @@ export const GameHeader = () => {
// 5. 좌측 상단 뒤로가기, 가운데 제목 차지하는 헤더 (API 호출 X) : 라운드 투표 현황
export const BackHeader = ({ title }: HeaderProps) => {
const navigate = useNavigate();

const focusRef = useFocus<HTMLElement>();
const goToBack = () => {
navigate(-1);
};

return (
<header css={headerLayout()}>
<header css={headerLayout()} tabIndex={0} ref={focusRef}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 질문 💭

tabIndex=0 을 한 이유가 무엇인가요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

<button onClick={goToBack} css={buttonWrapper}>
<img src={ArrowLeft} alt="뒤로 가기" />
</button>
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/hooks/useFocus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useEffect, useRef } from 'react';

const useFocus = <T extends HTMLElement>() => {
const focusRef = useRef<T>(null);

useEffect(() => {
if (focusRef.current) {
focusRef.current.focus();
}
}, []);

return focusRef;
};

export default useFocus;
4 changes: 3 additions & 1 deletion frontend/src/providers/ModalProvider/ModalProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { createContext, PropsWithChildren, useMemo, useState } from 'react';
import { createContext, PropsWithChildren, RefObject, useMemo, useState } from 'react';

interface ModalProps {
title?: string;
message?: string;
onConfirm?: () => void;
closeRef?: RefObject<HTMLElement>;
}

interface ModalState extends ModalProps {
Expand Down Expand Up @@ -39,6 +40,7 @@ const ModalProvider = ({ children }: PropsWithChildren) => {
message: props?.message,
onConfirm: props?.onConfirm,
isOpen: true,
closeRef: props?.closeRef,
});
};

Expand Down
Loading