Skip to content

Commit

Permalink
merge: 게임 화면 웹 접근성 개선 & 모바일 애니메이션 버벅임 해결 #321
Browse files Browse the repository at this point in the history
[REFACTOR] 게임 화면 웹 접근성 개선 & 모바일 애니메이션 버벅임 해결
  • Loading branch information
rbgksqkr authored Oct 16, 2024
2 parents 8367c96 + 94de7a0 commit b8b8abf
Show file tree
Hide file tree
Showing 19 changed files with 94 additions and 91 deletions.
Binary file modified frontend/src/assets/images/ddangkong.webp
Binary file not shown.
Binary file modified frontend/src/assets/images/ddangkongTimer.webp
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const SelectContainer = () => {
selectedOption={selectedOption}
handleClickOption={handleClickOption}
/>
<span>VS</span>
<span aria-hidden>VS</span>
<SelectOption
option={balanceContent.secondOption}
selectedOption={selectedOption}
Expand Down
29 changes: 22 additions & 7 deletions frontend/src/components/SelectContainer/Timer/Timer.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,24 @@ const shake = keyframes`
}
`;

const progress = keyframes`
0% {
transform: scaleX(1);
}
100% {
transform: scaleX(0);
}
`;

const timerTransition = keyframes`
0% {
transform: translateX(0);
}
100%{
transform: translateX(-95%);
}
`;

export const timerLayout = css`
display: flex;
position: relative;
Expand All @@ -42,7 +60,7 @@ export const timerLayout = css`
box-sizing: border-box;
`;

export const timerInnerLayout = (scale: number) => css`
export const timerInnerLayout = (timeLimit: number) => css`
display: flex;
justify-content: center;
align-items: center;
Expand All @@ -53,14 +71,12 @@ export const timerInnerLayout = (scale: number) => css`
background-color: ${Theme.color.peanut500};
transform: scaleX(${scale});
transform-origin: left;
transition: transform 1s linear;
animation: ${progress} ${timeLimit + 1}s linear;
`;

// 화면을 벗어나는 문제로 인해 100이 아닌 98로 계산
export const timerWrapper = (scale: number) => css`
export const timerWrapper = (timeLimit: number) => css`
display: flex;
position: absolute;
flex-direction: column;
Expand All @@ -70,8 +86,7 @@ export const timerWrapper = (scale: number) => css`
width: 100%;
height: 4rem;
transform: translateX(-${(1 - scale) * 98}%);
transition: transform 1s linear;
animation: ${timerTransition} ${timeLimit + 1}s linear;
`;

export const timerIcon = css`
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/components/SelectContainer/Timer/Timer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ describe('Timer 테스트', () => {
describe('Timer 훅 테스트', () => {
jest.useFakeTimers();
const voteMock = jest.fn();
const timeLimit = 10000;
const timeLimit = 10;
const timeLimitMs = timeLimit * 1000;

it('타이머가 종료되었을 때 선택 완료를 누르지 않아도 선택된 옵션이 있으면 투표한다.', () => {
const isSelectedOption = true;
Expand All @@ -31,7 +32,7 @@ describe('Timer 테스트', () => {
);

act(() => {
jest.advanceTimersByTime(timeLimit);
jest.advanceTimersByTime(timeLimitMs);
});

expect(result.current.leftRoundTime).toBe(0);
Expand All @@ -46,7 +47,7 @@ describe('Timer 테스트', () => {
);

act(() => {
jest.advanceTimersByTime(timeLimit);
jest.advanceTimersByTime(timeLimitMs);
});

expect(result.current.leftRoundTime).toBe(0);
Expand All @@ -61,7 +62,7 @@ describe('Timer 테스트', () => {
);

act(() => {
jest.advanceTimersByTime(timeLimit);
jest.advanceTimersByTime(timeLimitMs);
});

expect(result.current.leftRoundTime).toBe(0);
Expand Down
22 changes: 12 additions & 10 deletions frontend/src/components/SelectContainer/Timer/Timer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import {
timerText,
timerWrapper,
} from './Timer.styled';
import { formatLeftRoundTime } from './Timer.util';
import { convertMsecToSecond, formatLeftRoundTime, isAlertTimer } from './Timer.util';
import useVoteIsFinished from '../hooks/useVoteIsFinished';

import DdangkongTimer from '@/assets/images/ddangkongTimer.webp';
import A11yOnly from '@/components/common/a11yOnly/A11yOnly';
import useBalanceContentQuery from '@/hooks/useBalanceContentQuery';

interface TimerProps {
Expand All @@ -24,7 +25,7 @@ interface TimerProps {
const Timer = ({ selectedId, isVoted, completeSelection }: TimerProps) => {
const { roomId } = useParams();
const { balanceContent, isFetching } = useBalanceContentQuery(Number(roomId));
const { leftRoundTime, barScaleRate, isAlmostFinished } = useVoteTimer({
const { leftRoundTime, isAlmostFinished, timeLimit } = useVoteTimer({
roomId: Number(roomId),
selectedId,
isVoted,
Expand All @@ -38,14 +39,15 @@ const Timer = ({ selectedId, isVoted, completeSelection }: TimerProps) => {

return (
<section css={timerLayout}>
<div css={timerInnerLayout(barScaleRate)}></div>
<div css={timerWrapper(barScaleRate)}>
<img
css={[timerIcon, isAlmostFinished && timerIconShake]}
src={DdangkongTimer}
alt="타이머"
/>
<span css={timerText(isAlmostFinished)}>{formatLeftRoundTime(leftRoundTime)}</span>
<div css={timerInnerLayout(timeLimit)}></div>
<div css={timerWrapper(timeLimit)}>
<img css={[timerIcon, isAlmostFinished && timerIconShake]} src={DdangkongTimer} alt="" />
<A11yOnly aria-live={isAlertTimer(leftRoundTime, timeLimit) && 'polite'}>
{leftRoundTime}초 남았습니다.
</A11yOnly>
<span css={timerText(isAlmostFinished)} aria-hidden>
{formatLeftRoundTime(leftRoundTime)}
</span>
</div>
</section>
);
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/components/SelectContainer/Timer/Timer.util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { POLLING_DELAY } from '@/constants/config';
import { ALMOST_FINISH_SECOND, POLLING_DELAY } from '@/constants/config';

export const formatLeftRoundTime = (leftRoundTime: number) => {
const minutes = Math.floor(leftRoundTime / 60);
Expand All @@ -15,6 +15,7 @@ export const convertMsecToSecond = (msec: number) => {
return msec / UNIT_MSEC;
};

export const calculateUnitRate = (total: number, divisor: number) => {
return parseFloat((total / divisor).toFixed(1));
// Timer가 스크린 리더에 읽혀야하는 시점에 aria-live="polite" 설정하도록 boolean 값 반환 (제한 시간 절반 & 5초 남았을 때)
export const isAlertTimer = (leftRoundTime: number, timeLimit: number) => {
return leftRoundTime === Math.floor(timeLimit / 2) || leftRoundTime === ALMOST_FINISH_SECOND;
};
17 changes: 3 additions & 14 deletions frontend/src/components/SelectContainer/Timer/hooks/useTimer.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { useEffect, useRef, useState } from 'react';

import { calculateUnitRate, convertMsecToSecond } from '../Timer.util';

import { POLLING_DELAY } from '@/constants/config';

const INITIAL_BAR_SCALE = 1;
const ALMOST_FINISH_SECOND = 5;
import { ALMOST_FINISH_SECOND, POLLING_DELAY } from '@/constants/config';

interface UseTimerProps {
timeLimit: number;
Expand All @@ -15,8 +10,7 @@ interface UseTimerProps {
}

const useTimer = ({ timeLimit, isSelectedOption, isVoted, vote }: UseTimerProps) => {
const [leftRoundTime, setLeftRoundTime] = useState(convertMsecToSecond(timeLimit));
const [barScaleRate, setBarScaleRate] = useState(INITIAL_BAR_SCALE);
const [leftRoundTime, setLeftRoundTime] = useState(timeLimit);

const isVoteTimeout = leftRoundTime <= 0;
const isAlmostFinished = leftRoundTime <= ALMOST_FINISH_SECOND;
Expand All @@ -34,21 +28,16 @@ const useTimer = ({ timeLimit, isSelectedOption, isVoted, vote }: UseTimerProps)
}, [isVoteTimeout, isSelectedOption, isVoted, vote]);

useEffect(() => {
const timeLimitPerSecond = convertMsecToSecond(timeLimit);
const decreaseRate = calculateUnitRate(INITIAL_BAR_SCALE, timeLimitPerSecond);
setLeftRoundTime(timeLimitPerSecond);

timeout.current = setInterval(() => {
setLeftRoundTime((prev) => prev - 1);
setBarScaleRate((prevRate) => (prevRate > 0 ? prevRate - decreaseRate : 0));
}, POLLING_DELAY);

return () => {
clearInterval(timeout.current);
};
}, [timeLimit]);

return { leftRoundTime, barScaleRate, isAlmostFinished };
return { leftRoundTime, isAlmostFinished };
};

export default useTimer;
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import useTimer from './useTimer';
import { convertMsecToSecond } from '../Timer.util';

import useCompleteSelectionMutation from '@/components/common/SelectButton/SelectButton.hook';
import useBalanceContentQuery from '@/hooks/useBalanceContentQuery';

const DEFAULT_TIME_LIMIT_MSEC = 10000;
const DEFAULT_TIME_LIMIT_SEC = 10;

interface UseVoteTimerProps {
roomId: number;
Expand All @@ -14,22 +15,22 @@ interface UseVoteTimerProps {

const useVoteTimer = ({ roomId, selectedId, isVoted, completeSelection }: UseVoteTimerProps) => {
const { balanceContent } = useBalanceContentQuery(roomId);
const timeLimit = balanceContent.timeLimit || DEFAULT_TIME_LIMIT_MSEC;
const timeLimit = convertMsecToSecond(balanceContent.timeLimit) || DEFAULT_TIME_LIMIT_SEC;

const { mutate: vote } = useCompleteSelectionMutation({
selectedId,
contentId: balanceContent.contentId,
completeSelection,
});

const { leftRoundTime, barScaleRate, isAlmostFinished } = useTimer({
const { leftRoundTime, isAlmostFinished } = useTimer({
timeLimit,
isSelectedOption: Boolean(selectedId),
isVoted,
vote,
});

return { leftRoundTime, barScaleRate, isAlmostFinished };
return { leftRoundTime, isAlmostFinished, timeLimit };
};

export default useVoteTimer;
3 changes: 2 additions & 1 deletion frontend/src/components/SelectOption/SelectOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ const SelectOption = ({ option, selectedOption, handleClickOption }: SelectOptio

return (
<button
css={SelectOptionLayout(Boolean(selectedId === option.optionId), isCompleted)}
css={SelectOptionLayout(selectedId === option.optionId, isCompleted)}
onClick={() => handleClickOption(option.optionId)}
disabled={isCompleted}
aria-pressed={selectedId === option.optionId}
>
{option.name}
</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Countdown from './Countdown/Countdown';
import useCountdown from './hooks/useCountdown';
import StartButton from './StartButton/StartButton';
import useCountdown from '../hooks/useCountdown';

import { useGetRoomInfo } from '@/hooks/useGetRoomInfo';

Expand Down
12 changes: 10 additions & 2 deletions frontend/src/components/TopicContainer/TopicContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useLocation, useParams } from 'react-router-dom';

import { categoryText, topicContainerLayout, topicText } from './TopicContainer.styled';
import A11yOnly from '../common/a11yOnly/A11yOnly';

import { ROUTES } from '@/constants/routes';
import useBalanceContentQuery from '@/hooks/useBalanceContentQuery';
Expand All @@ -12,10 +13,17 @@ const TopicContainer = () => {

const isGamePage = location.pathname === ROUTES.game(Number(roomId));

const screenReaderQuestion = `질문. ${balanceContent.question}.`;

return (
<section css={topicContainerLayout}>
<span css={categoryText}>{isGamePage && balanceContent.category}</span>
<span css={topicText}>{balanceContent.question}</span>
<A11yOnly>{screenReaderQuestion}</A11yOnly>
<span css={categoryText} aria-hidden>
{isGamePage && balanceContent.category}
</span>
<span css={topicText} aria-hidden>
{balanceContent.question}
</span>
</section>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { captureException, withScope } from '@sentry/react';
import { useNavigate } from 'react-router-dom';

import Button from '../../Button/Button';
import {
Expand All @@ -17,10 +16,8 @@ interface RootErrorFallbackProps {
}

const RootErrorFallback = ({ error, resetError }: RootErrorFallbackProps) => {
const navigate = useNavigate();

const goToHome = () => {
navigate('/');
window.location.href = '/';
};

if (error instanceof Error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const SelectButton = ({ contentId, selectedId, completeSelection }: SelectButton
disabled={data || !selectedId || isPending}
text={data || isPending ? '선택 완료' : '선택'}
onClick={vote}
aria-pressed={data}
/>
</div>
);
Expand Down
32 changes: 0 additions & 32 deletions frontend/src/components/hooks/useCountdown.ts

This file was deleted.

Loading

0 comments on commit b8b8abf

Please sign in to comment.