Skip to content

Commit

Permalink
[FE] 약속 입장, 약속 현황 조회 페이지(달력)의 웹 접근성을 개선 (#421)
Browse files Browse the repository at this point in the history
* feat(Header): iOS 보이스오버 접근성 개선을 위해 페이지 진입 시 h1에 포커스 추가

* feat(Header): h1 요소의 포커스 시 파란색 테두리 제거 및 클릭 방지 설정 추가

* refactor: logo, sunglass img 포맷을 svg → webp로 수정

* feat: aria-label을 사용하여 버튼의 기능을 명확히 읽어줄 수 있게 개선

* feat(MeetingEntrancePage): 약속명 읽기 개선을 위해 aria-label 추가

* feat(Text): role="text" 사용하여 텍스트 분리 읽기 문제 개선

* feat: 약속 현황 조회 페이지의 참여자 탭 클릭 시 aria-label 업데이트

참여자 탭을 클릭할 때마다 aria-label에 '참여자가 선택한 일정 확인하기' 문구를 추가하여 스크린 리더 사용자에게 현재 선택된 상태를 명확히 전달하도록 개선

* feat(MeetingCalendar): 약속 현황 조회 페이지 달력 접근성 개선

* fix(Calendar): storybook에서 ToastProvider 적용 안 되는 문제 해결

.storybook/preview.tsx에서 모든 스토리에 대해 ToastProvider로 컴포넌트를 감싸도록 설정

* rename: index.tsx로 파일명 수정
  • Loading branch information
Yoonkyoungme authored Oct 24, 2024
1 parent cf21b3d commit bce13c0
Show file tree
Hide file tree
Showing 38 changed files with 205 additions and 59 deletions.
1 change: 1 addition & 0 deletions frontend/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"plugin:storybook/recommended"
],
"rules": {
"react/prop-types": "off",
"@typescript-eslint/consistent-type-imports": "error",
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/no-noninteractive-element-interactions": "off",
Expand Down
7 changes: 5 additions & 2 deletions frontend/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Global, ThemeProvider } from '@emotion/react';
import type { Preview } from '@storybook/react';
import React from 'react';

import ToastProvider from '../src/contexts/ToastProvider';
import globalStyles from '../src/styles/global';
import theme from '../src/styles/theme';

Expand All @@ -21,8 +22,10 @@ export default preview;
export const decorators = [
(Story) => (
<ThemeProvider theme={theme}>
<Global styles={globalStyles} />
<Story />
<ToastProvider>
<Global styles={globalStyles} />
<Story />
</ToastProvider>
</ThemeProvider>
),
];
4 changes: 3 additions & 1 deletion frontend/legacy/components/AllSchedules/AllSchdules.utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { DAY_OF_WEEK_KR } from '../../../src/constants/date';

export const formatDate = (dateString: string) => {
const currentDateObj = new Date(dateString);
const currentMonth = currentDateObj.getMonth() + 1;
const currentDay = currentDateObj.getDate();
const dayOfWeek = ['일', '월', '화', '수', '목', '금', '토'][currentDateObj.getDay()];
const dayOfWeek = DAY_OF_WEEK_KR[currentDateObj.getDay()];

return {
dayOfWeek,
Expand Down
9 changes: 0 additions & 9 deletions frontend/src/assets/images/logo.svg

This file was deleted.

Binary file added frontend/src/assets/images/logo.webp
Binary file not shown.
9 changes: 0 additions & 9 deletions frontend/src/assets/images/logoSunglass.svg

This file was deleted.

Binary file added frontend/src/assets/images/logoSunglass.webp
Binary file not shown.
2 changes: 2 additions & 0 deletions frontend/src/components/MeetingCalendar/Date/Date.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getHolidayNames } from '@hyunbinseo/holidays-kr';
import { getDate, getDay, getFullDate } from '@utils/date';

import CALENDAR_PROPERTIES from '@constants/calendar';
import { DAY_OF_WEEK_KR } from '@constants/date';

export const getDateInfo = (targetDate: Date, today: Date) => {
const targetDateNumber = getDate(targetDate);
Expand All @@ -28,6 +29,7 @@ export const getDateInfo = (targetDate: Date, today: Date) => {
return {
date: targetDateNumber,
targetFullDate,
targetDayOfWeekKR: DAY_OF_WEEK_KR[targetDayOfWeek],
isHoliday,
isToday,
isSunday,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { PropsWithChildren } from 'react';

import {
s_monthNavigation,
s_monthNavigationContainer,
Expand All @@ -23,7 +25,10 @@ export default function Header({
moveToNextMonth,
moveToPrevMonth,
isCurrentMonth,
}: HeaderProps) {
children,
}: PropsWithChildren<HeaderProps>) {
const yearMonthText = `${currentYear}${currentMonth + 1}월`;

return (
<header css={s_container}>
<div css={s_monthNavigationContainer}>
Expand All @@ -35,9 +40,9 @@ export default function Header({
>
{'<'}
</button>

<span css={s_yearMonthText}>
{currentYear}{currentMonth + 1}
<span css={s_yearMonthText} aria-live="polite" role="text">
{yearMonthText}
{children}
</span>
<button css={s_monthNavigation} onClick={moveToNextMonth} aria-label="다음 달">
{'>'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import { usePostScheduleByMode } from '@stores/servers/schedule/mutations';

import { getFullDate } from '@utils/date';

import Header from '../Header/Header';
import SingleDate from '../SingleDate/SingleDate';
import Header from '../Header';
import SingleDate from '../SingleDate';
import WeekDays from '../WeekDays';

interface PickerProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
} from '@components/MeetingCalendar/Date/Date.styles';
import { getDateInfo } from '@components/MeetingCalendar/Date/Date.utils';

import { formatAriaFullDate } from '@utils/a11y';

import Check from '@assets/images/attendeeCheck.svg';

import { s_additionalText, s_viewer } from './SingleDate.styles';
Expand All @@ -30,7 +32,16 @@ export default function SingleDateViewer({
availableAttendees,
}: DateProps) {
const { value, status } = dateInfo;
const { date, isHoliday, isToday, isSaturday, isSunday, isPrevDate } = getDateInfo(value, today);
const {
date,
targetFullDate,
targetDayOfWeekKR,
isHoliday,
isToday,
isSaturday,
isSunday,
isPrevDate,
} = getDateInfo(value, today);

const additionalText = () => {
if (!availableAttendees) return '\u00A0';
Expand All @@ -43,8 +54,7 @@ export default function SingleDateViewer({
availableAttendees && <AttendeeTooltip attendeeNames={availableAttendees} position="top" />;

return status === 'current' ? (
<button
disabled={!isAvailable || isPrevDate}
<div
css={[
s_dateContainer,
s_baseDateButton,
Expand All @@ -58,11 +68,16 @@ export default function SingleDateViewer({
isSaturday,
}),
]}
role="text"
aria-hidden={!isAvailable}
aria-label={
isAvailable ? formatAriaFullDate(targetFullDate, targetDayOfWeekKR, availableAttendees) : ''
}
>
<span css={s_baseDateText}>{date}</span>
<span css={s_additionalText}>{additionalText()}</span>
{renderTooltip()}
</button>
</div>
) : (
<div css={s_dateContainer}></div>
);
Expand Down
31 changes: 27 additions & 4 deletions frontend/src/components/MeetingConfirmCalendar/Viewer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,19 @@ import {
import { Button } from '@components/_common/Buttons/Button';
import TabButton from '@components/_common/Buttons/TabButton';
import Calendar from '@components/_common/Calendar';
import ScreenReaderOnly from '@components/_common/ScreenReaderOnly';

import useRouter from '@hooks/useRouter/useRouter';

import { useGetSchedules } from '@stores/servers/schedule/queries';

import { getFullDate } from '@utils/date';
import { formatAriaTab } from '@utils/a11y';
import { getFullDate, hasSelectableDaysInMonth } from '@utils/date';

import Check from '@assets/images/attendeeCheck.svg';
import Pen from '@assets/images/pen.svg';

import Header from '../Header/Header';
import Header from '../Header';
import SingleDateViewer from '../SingleDate/SingleDateViewer';
import WeekDays from '../WeekDays';

Expand Down Expand Up @@ -84,6 +86,7 @@ export default function Viewer({
tabButtonVariants="outlinedFloating"
onClick={() => setSelectedAttendee('')}
isActive={selectedAttendee === ''}
aria-label={formatAriaTab('전체', selectedAttendee === '')}
>
{selectedAttendee === '' && <Check width="12" height="12" />}
전체
Expand All @@ -94,6 +97,7 @@ export default function Viewer({
tabButtonVariants="outlinedFloating"
onClick={() => setSelectedAttendee(attendee)}
isActive={selectedAttendee === attendee}
aria-label={formatAriaTab(attendee, selectedAttendee === attendee)}
>
{selectedAttendee === attendee && <Check width="12" height="12" />}
{attendee}
Expand All @@ -102,8 +106,22 @@ export default function Viewer({
</section>

<Calendar>
<Calendar.Header render={(props) => <Header {...props} />} />
<Calendar.Header
render={(props) => (
<Header {...props}>
<ScreenReaderOnly>
{hasSelectableDaysInMonth(
props.currentMonth,
meetingSchedules.schedules.map(({ date }) => date),
)
? '선택 가능한 날이 있습니다.'
: '선택 가능한 날이 없습니다.'}
</ScreenReaderOnly>
</Header>
)}
/>
<Calendar.WeekDays render={(weekdays) => <WeekDays weekdays={weekdays} />} />

<Calendar.Body
renderDate={(dateInfo, today) => (
<SingleDateViewer
Expand Down Expand Up @@ -138,7 +156,12 @@ export default function Viewer({
</Button>
)}
</div>
<button disabled={isLocked} onClick={handleScheduleUpdate} css={s_circleButton}>
<button
disabled={isLocked}
onClick={handleScheduleUpdate}
css={s_circleButton}
aria-label="약속 수정하기"
>
<Pen width="28" height="28" />
</button>
</footer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface WeekDaysProps {

export default function WeekDays({ weekdays }: WeekDaysProps) {
return (
<section css={[s_calendarContent, s_dayOfWeekContainer]}>
<section css={[s_calendarContent, s_dayOfWeekContainer]} aria-hidden={true}>
{weekdays.map((day, index) => (
<div key={day} css={[s_baseDayOfWeek, s_dayOfWeek(index)]}>
{day}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import TabButton from '@components/_common/Buttons/TabButton';
import useRouter from '@hooks/useRouter/useRouter';
import useSelectSchedule from '@hooks/useSelectSchedule/useSelectSchedule';

import { formatAriaTab } from '@utils/a11y';

import Check from '@assets/images/attendeeCheck.svg';
import Pen from '@assets/images/pen.svg';

Expand Down Expand Up @@ -73,6 +75,7 @@ export default function SchedulesViewer({
tabButtonVariants="outlinedFloating"
onClick={() => handleAttendeeChange('')}
isActive={selectedAttendee === ''}
aria-label={formatAriaTab('전체', selectedAttendee === '')}
>
{selectedAttendee === '' && <Check width="12" height="12" />}
전체
Expand All @@ -83,6 +86,7 @@ export default function SchedulesViewer({
tabButtonVariants="outlinedFloating"
onClick={() => handleAttendeeChange(attendee)}
isActive={selectedAttendee === attendee}
aria-label={formatAriaTab(attendee, selectedAttendee === attendee)}
>
{selectedAttendee === attendee && <Check width="12" height="12" />}
{attendee}
Expand Down Expand Up @@ -127,7 +131,12 @@ export default function SchedulesViewer({
</Button>
)}
</div>
<button disabled={isLocked} onClick={handleScheduleUpdate} css={s_circleButton}>
<button
disabled={isLocked}
onClick={handleScheduleUpdate}
css={s_circleButton}
aria-label="약속 수정하기"
>
<Pen width="28" height="28" />
</button>
</footer>
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/Schedules/Schedules.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import type {
} from 'types/schedule';
import type { TooltipPosition } from 'types/tooltip';

import { DAY_OF_WEEK_KR } from '@constants/date';

export const formatDate = (dateString: string) => {
const currentDateObj = new Date(dateString);
const currentMonth = currentDateObj.getMonth() + 1;
const currentDay = currentDateObj.getDate();
const dayOfWeek = ['일', '월', '화', '수', '목', '금', '토'][currentDateObj.getDay()];
const dayOfWeek = DAY_OF_WEEK_KR[currentDateObj.getDay()];

return {
dayOfWeek,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default function BackButton({ path }: BackButtonProps) {
};

return (
<button css={s_headerIconButton} onClick={handleBackButtonClick}>
<button css={s_headerIconButton} onClick={handleBackButtonClick} aria-label="뒤로가기">
<BackSVG width="24" height="24" />
</button>
);
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/components/_common/Buttons/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
isLoading?: boolean;
customCss?: SerializedStyles;
ariaLabel?: string;
}

export function Button({
Expand All @@ -27,13 +28,20 @@ export function Button({
customCss,
type = 'button',
onClick,
'aria-label': ariaLabel,
}: ButtonProps) {
const cssProps = [s_baseButton(borderRadius), s_size(size)];

if (variant) cssProps.push(s_variant[variant]);

return (
<button disabled={disabled} css={[cssProps, customCss]} type={type} onClick={onClick}>
<button
disabled={disabled}
css={[cssProps, customCss]}
type={type}
onClick={onClick}
aria-label={ariaLabel}
>
{isLoading && <Spinner backgroundColor={'#f4f4f5'} />}
{!isLoading && children}
</button>
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/components/_common/Calendar/Body/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// import 하지 않으면 스토리북에서 캘린더 컴포넌트가 렌더링 되지 않아 일단 추가(@해리)
import React from 'react';
import type { DateInfo } from 'types/calendar';

import { useCalendarContext } from '@hooks/useCalendarContext/useCalendarContext';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import Calendar from '.';
const meta: Meta<typeof Calendar> = {
title: 'Components/Calendar',
component: Calendar,
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/components/_common/Calendar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import type { PropsWithChildren } from 'react';
// import 하지 않으면 스토리북에서 캘린더 컴포넌트가 렌더링 되지 않아 일단 추가(@해리)
import React from 'react';

import CalendarProvider from '@contexts/CalendarProvider';

Expand Down
7 changes: 7 additions & 0 deletions frontend/src/components/_common/Header/Header.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,15 @@ export const s_header = css`
`;

export const s_title = css`
pointer-events: none;
position: absolute;
left: 50%;
transform: translateX(-50%);
font-weight: ${theme.typography.bodyBold};
&:focus {
outline: none;
}
`;
12 changes: 10 additions & 2 deletions frontend/src/components/_common/Header/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PropsWithChildren } from 'react';
import { type PropsWithChildren, useEffect, useRef } from 'react';

import { s_header, s_title } from './Header.styles';

Expand All @@ -7,10 +7,18 @@ interface HeaderProps {
}

export default function Header({ title, children }: PropsWithChildren<HeaderProps>) {
const titleRef = useRef<HTMLHeadingElement>(null);

useEffect(() => {
titleRef.current?.focus();
}, []);

return (
<header css={s_header}>
{children}
<h1 css={s_title}>{title}</h1>
<h1 css={s_title} ref={titleRef} tabIndex={-1}>
{title}
</h1>
</header>
);
}
Loading

0 comments on commit bce13c0

Please sign in to comment.