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

[Feature] - 메인 페이지, 로그인 페이지 웹 접근성 개선 #529

Open
wants to merge 49 commits into
base: develop/fe
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
53f180a
refactor(Header): 헤더에 사용된 IconButton들에 aria-label 추가
0jenn0 Oct 1, 2024
e46a62a
refactor(Drawer): button 태그 중첩 사용 수정
0jenn0 Oct 1, 2024
0d0b8d4
chore(MainPage): 필요 없는 공백,태그 삭제
0jenn0 Oct 1, 2024
af6ffd4
refactor(AvatarCircle): props 수정
0jenn0 Oct 7, 2024
28d74fd
refactor(TravelogueCard): 웹 접근성 개선
0jenn0 Oct 7, 2024
1e70067
refactor(Chip): as props 추가
0jenn0 Oct 8, 2024
a82b9a9
style(FallbackImage): color contrast 개선
0jenn0 Oct 9, 2024
6e32023
refactor(MainPage): Chip을 button으로 사용하도록 수정
0jenn0 Oct 9, 2024
2de64e0
refactor(FloatingButton): 플로팅 버튼 title 추가
0jenn0 Oct 10, 2024
e490f5e
feat(FocusTrap): 키보드 트랩 hook 구현
0jenn0 Oct 10, 2024
903206e
refactor(Modal): createPortal 위치를 #root에서 body로 변경
0jenn0 Oct 10, 2024
d507bfc
refactor(FocusTrap): onEscapeFocusTrap을 옵셔널로 수정
0jenn0 Oct 10, 2024
e37935c
feat(Header): 키보드 트랩 적용
0jenn0 Oct 10, 2024
1b6e35b
refactor(Header): 시맨틱 태그 수정
0jenn0 Oct 10, 2024
5a1edf1
feat(Drawer): esc로 닫기 추가 및 열려있을 경우에만 DrawerContainer렌더링하도록 수정
0jenn0 Oct 10, 2024
a04e8dd
feat(FocusTrap): 첫번째 요소에 자동 포커스 삭제
0jenn0 Oct 10, 2024
72ff036
feat(Modal): 모달에 FocusTrap 적용
0jenn0 Oct 10, 2024
1810ce1
feat(MainPage): 시맨틱 태그 개선
0jenn0 Oct 10, 2024
dcd103a
feat(MainPage): 모달 열림 닫힘 알림 추가
0jenn0 Oct 10, 2024
d091c40
feat(removeEmojis): 이모지를 지우고 string만 반환하는 유틸 함수 추가
0jenn0 Oct 10, 2024
c24d50c
refactor(TravelogueCard): removeEmojis 유틸 함수 사용
0jenn0 Oct 10, 2024
5931297
refactor(FloatingButton): 플로팅 버튼에 focus trap 사용
0jenn0 Oct 10, 2024
8a08609
style(FloatingButton): visual hidden 스타일 추가
0jenn0 Oct 10, 2024
735d281
feat(VisuallyHidden): Visually hidden 컴포넌트 추가
0jenn0 Oct 12, 2024
5760fdd
feat(MainPage): 태그 선택,해제시 알림 추가
0jenn0 Oct 12, 2024
67dad24
feat(MainPage): TravelogueCard 리스트 시맨틱 태그 개선
0jenn0 Oct 12, 2024
3a923ef
fix(TravelogueCard): aria-live 속성 삭제
0jenn0 Oct 12, 2024
1b5e434
feat(MainPage): 모달 열/닫힘 알림에 VisuallyHidden 컴포넌트 사용
0jenn0 Oct 12, 2024
2e92906
refactor(Drawer): Trigger 시맨틱 태그 및 props 수정
0jenn0 Oct 13, 2024
81ba587
refactor(Header): 시맨틱 태그 수정 및 button 개선
0jenn0 Oct 13, 2024
e8f4a4c
feat(MainPage): 여행기 로드시 새로 로드된 여행기에 focus되기 구현
0jenn0 Oct 13, 2024
c92ada7
feat(MainPage): fetchButton으로 로드시 알림 및 태그 선택시 알람 추가
0jenn0 Oct 13, 2024
7ac5c26
feat(Drawer): 사용자 메뉴 모달 열/닫힘 안내 추가
0jenn0 Oct 13, 2024
623cadc
fix(TravelogueCard): 여행기 제목 읽을 시 이모지 삭제
0jenn0 Oct 13, 2024
3e519ca
refactor(FloatingButton): VisuallyHidden 컴포넌트 사용으로 수정
0jenn0 Oct 14, 2024
d3b4654
refactor(FloatingButton): 상수 분리
0jenn0 Oct 14, 2024
d998713
refactor(Chip): 화살표 함수로 수정 및 논리 연산자 수정
0jenn0 Oct 14, 2024
b524e4b
refactor(removeEmojis): 함수 책임 간소화
0jenn0 Oct 14, 2024
517a969
chore(FocusTrap): 불필요한 주석 삭제
0jenn0 Oct 14, 2024
c5b0733
refactor(FocusTrap): 사용되지 않는 값들 삭제
0jenn0 Oct 14, 2024
2db1cb2
refactor(SearchHeader): 불필요한 option 삭제
0jenn0 Oct 14, 2024
2c4b741
refactor(Drawer): usePressESC 훅 사용
0jenn0 Oct 14, 2024
85e2886
refactor(Drawer): styled component로 수정
0jenn0 Oct 14, 2024
ee46e07
refactor(FloatingButton): 클로저 삭제
0jenn0 Oct 14, 2024
10a5943
chore(Header): 사용하지 않는 styled component 삭제
0jenn0 Oct 14, 2024
f79737b
styled(MainPage): theme 사용
0jenn0 Oct 14, 2024
8d32894
refactor(FloatingButton): 알림 텍스트 수정
0jenn0 Oct 14, 2024
8c5d737
refactor(LoginPage): 상수 파일 분리
0jenn0 Oct 15, 2024
3974935
feat(LoginPage): 로그인 페이지 접속시 로그인 버튼에 focus되어있기 구현
0jenn0 Oct 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@ import useImageError from "@hooks/useImageError";
import * as S from "./AvatarCircle.styled";
import type { AvatarCircleSize } from "./AvatarCircle.type";

interface AvatarCircleProps {
interface AvatarCircleProps extends React.ImgHTMLAttributes<HTMLImageElement> {
Copy link
Contributor

Choose a reason for hiding this comment

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

ImgHTMLAttributes로 alt나 className같은 속성들을 외부에서 받도록 처리했네요! 좋은거 같아요 👍👍👍

Choose a reason for hiding this comment

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

좋네요 저도 onLoad를 이용해야했는데 해당 방법으로 수정하니 유연하게 대응할 수 있었습니다 !

$size?: AvatarCircleSize;
profileImageUrl?: string;
imageAlt?: string;
}

const AvatarCircle = ({ $size = "small", profileImageUrl, imageAlt }: AvatarCircleProps) => {
const AvatarCircle = ({ $size = "small", profileImageUrl, ...props }: AvatarCircleProps) => {
const { imageError, handleImageError } = useImageError({ imageUrl: profileImageUrl });

return (
<S.AvatarCircleContainer $size={$size}>
{!imageError ? (
<img src={profileImageUrl} alt={imageAlt} onError={handleImageError} />
<img src={profileImageUrl} onError={handleImageError} {...props} />
) : (
<S.FallbackIcon $size={$size}>
<svg
Expand Down
22 changes: 18 additions & 4 deletions frontend/src/components/common/Chip/Chip.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
import { ComponentPropsWithoutRef } from "react";
import React from "react";

import { CYPRESS_DATA_MAP } from "@constants/cypress";

import Text from "../Text/Text";
import * as S from "./Chip.styled";
import { DEFAULT_ELEMENT } from "./constants";

interface ChipProps extends ComponentPropsWithoutRef<"li"> {
interface ChipOwnProps<Element extends React.ElementType = typeof DEFAULT_ELEMENT> {
as?: Element;
label: string;
isSelected?: boolean;
index?: number;
}

const Chip = ({ label, isSelected = false, index, children, ...props }: ChipProps) => {
type ChipProps<E extends React.ElementType> = ChipOwnProps<E> &
Omit<React.ComponentPropsWithoutRef<E>, keyof ChipOwnProps>;

const Chip = <E extends React.ElementType>({
as,
label,
isSelected = false,
index,
children,
...props
}: ChipProps<E>) => {
const Component = as ?? DEFAULT_ELEMENT;

return (
<S.Layout
as={Component}
$isSelected={isSelected}
$index={index}
data-cy={isSelected ? `selected-${CYPRESS_DATA_MAP.chip}` : CYPRESS_DATA_MAP.chip}
Expand All @@ -24,5 +39,4 @@ const Chip = ({ label, isSelected = false, index, children, ...props }: ChipProp
</S.Layout>
);
};

export default Chip;
1 change: 1 addition & 0 deletions frontend/src/components/common/Chip/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DEFAULT_ELEMENT = "li" as const;
39 changes: 30 additions & 9 deletions frontend/src/components/common/Drawer/Drawer.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import React, { useState } from "react";
import React, { useCallback, useState } from "react";
import ReactDOM from "react-dom";

import DrawerProvider, { useDrawerContext } from "@contexts/DrawerProvider";

import useModalControl from "@hooks/useModalControl";
import usePressESC from "@hooks/usePressESC";

import VisuallyHidden from "../VisuallyHidden/VisuallyHidden";
import * as S from "./Drawer.styled";

const Drawer = ({ children }: React.PropsWithChildren) => {
const [isOpen, setIsOpen] = useState(false);

const toggleDrawer = () => setIsOpen((prev) => !prev);
const toggleDrawer = useCallback(() => setIsOpen((prev) => !prev), []);
Comment on lines 13 to +14

Choose a reason for hiding this comment

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

useToggle에 toggle 함수 만들고 해당 훅 이용하면 더 좋을 거 같아요!

usePressESC(isOpen, toggleDrawer);

useModalControl(isOpen, toggleDrawer);

Expand All @@ -31,12 +34,19 @@ const Drawer = ({ children }: React.PropsWithChildren) => {

return (
<DrawerProvider isOpen={isOpen} toggleDrawer={toggleDrawer}>
<VisuallyHidden aria-live="assertive">
{isOpen ? "사용자 메뉴 모달이 열렸습니다." : "사용자 메뉴 모달이 닫혔습니다."}

Choose a reason for hiding this comment

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

개인적으로 모달이라는 용어를 일반인이 알기는 힘들 거 같다는 생각이 들어요 그래서 사용자 메뉴가 열렸습니다 닫혔습니다 정도로 바꾸면 더 좋을 거 같아요

</VisuallyHidden>
{otherContent}
<S.Overlay isOpen={isOpen} onClick={toggleDrawer} />
<S.DrawerContainer isOpen={isOpen}>
{headerContent}
{drawerContent}
</S.DrawerContainer>
{isOpen &&
ReactDOM.createPortal(
<S.DrawerContainer id="drawer-content" isOpen={isOpen} aria-modal="true" role="dialog">
{headerContent}
{drawerContent}
</S.DrawerContainer>,
document.body,
)}
</DrawerProvider>
);
};
Expand All @@ -49,10 +59,21 @@ const Content = ({ children }: React.PropsWithChildren) => {
return <S.DrawerContent>{children}</S.DrawerContent>;
};

const Trigger = ({ children }: React.PropsWithChildren) => {
interface TriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
}

Comment on lines +62 to +66

Choose a reason for hiding this comment

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

interface TriggerProps extends PropsWithChildren<React.ButtonHTMLAttributes> {
onClick?: (event: React.MouseEvent) => void;
} 요렇게 수정하면 더 좋을 것 같아요

const Trigger = ({ children, onClick }: TriggerProps) => {
const { toggleDrawer } = useDrawerContext();

const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
toggleDrawer();
onClick?.(event);
};

return (
<S.TriggerButton onClick={toggleDrawer} aria-label="Toggle drawer">
<S.TriggerButton type="button" onClick={handleClick}>
{children}
</S.TriggerButton>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ export const Fallback = styled.div`
background-color: #eee;

${(props) => props.theme.typography.mobile.detailBold};
color: #9e9e9e;
color: ${({ theme }) => theme.colors.text.secondary};
Copy link
Contributor

Choose a reason for hiding this comment

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

오오 color 바꿔주신부분 좋아요! 다만 해당 컬러는 gray[500]이고, text secondary는 gray[700]인걸로 파악했습니다. 혹시 이 컬러로 변경한 이유가 있을까요?!

Copy link
Author

Choose a reason for hiding this comment

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

아아 이 부분에서 색상 대조 이슈가 생기더구라욥
gray[700]부터 색상 대조 이슈가 해결되서 이 색으로 수정했습니다

`;
58 changes: 37 additions & 21 deletions frontend/src/components/common/FloatingButton/FloatingButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import { useNavigate } from "react-router-dom";

import useModalControl from "@hooks/useModalControl";

import { ROUTE_PATHS_MAP } from "@constants/route";
import { removeEmoji } from "@utils/removeEmojis";

import { PRIMITIVE_COLORS } from "@styles/tokens";

import FocusTrap from "../FocusTrap";
import IconButton from "../IconButton/IconButton";
import Text from "../Text/Text";
import VisuallyHidden from "../VisuallyHidden/VisuallyHidden";
import * as S from "./FloatingButton.styled";
import SUB_BUTTONS from "./constants";

const FloatingButton = () => {
const [isOpen, setIsOpen] = useState(false);
Expand All @@ -19,34 +22,47 @@ const FloatingButton = () => {
setIsOpen((prev) => !prev);
};

Choose a reason for hiding this comment

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

본 김에 이 친구도 useToggle 이용해보면 좋을 것 같습니다!


const handleClickTravelogueRegister = () => {
navigate(ROUTE_PATHS_MAP.travelogueRegister);
};

const handleClickTravelPlanRegister = () => {
navigate(ROUTE_PATHS_MAP.travelPlanRegister);
const handleClickSubButton = (route: string) => {
navigate(route);
};

useModalControl(isOpen, handleToggleButton);

return (
<S.FloatingButtonContainer>
{isOpen && <S.BackdropLayout onClick={handleToggleButton} />}
<S.SubButtonContainer $isOpen={isOpen}>
<S.SubButton onClick={handleClickTravelPlanRegister}>
<Text textType="body" css={S.subButtonTextStyle}>
✈️ 여행 계획 작성
</Text>
</S.SubButton>
<S.SubButton onClick={handleClickTravelogueRegister}>
<Text textType="body" css={S.subButtonTextStyle}>
📝 여행기 작성
</Text>
</S.SubButton>
</S.SubButtonContainer>
<VisuallyHidden aria-live="assertive">
{isOpen
? "여행기 및 여행 계획 작성 플로팅 버튼이 열렸습니다. 닫으려면 esc버튼을 눌러주세요."
: "여행기 및 여행 계획 작성 플로팅 버튼이 닫혔습니다."}
Comment on lines +35 to +36

Choose a reason for hiding this comment

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

얘도 플로팅 버튼을 직접적으로 쓰면 모를 거 같아요. 여행기 및 여행 계획 작성 메뉴가 열렸습니다 정도면 어떨까요?

</VisuallyHidden>
{isOpen && (
<>
<S.BackdropLayout onClick={handleToggleButton} />
<FocusTrap>
<S.SubButtonContainer $isOpen={isOpen}>
Copy link
Contributor

Choose a reason for hiding this comment

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

SubButton 묶어놓은거 좋네요!

{SUB_BUTTONS.map(({ text, route }) => (
<S.SubButton
key={route}
onClick={() => handleClickSubButton(route)}
aria-label={removeEmoji(text)}
>
<Text textType="body" css={S.subButtonTextStyle}>
{text}
</Text>
</S.SubButton>
))}
</S.SubButtonContainer>
</FocusTrap>
</>
)}

<S.MainButtonWrapper onClick={handleToggleButton} $isOpen={isOpen}>
<IconButton iconType="plus" color={PRIMITIVE_COLORS.white} size="20" />
<IconButton
iconType="plus"
color={PRIMITIVE_COLORS.white}
size="20"
title="여행 계획 및 여행기 작성 플로팅"
/>
</S.MainButtonWrapper>
</S.FloatingButtonContainer>
);
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/components/common/FloatingButton/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ROUTE_PATHS_MAP } from "@constants/route";

const SUB_BUTTONS = [
{
text: "✈️ 여행 계획 작성",
route: ROUTE_PATHS_MAP.travelPlanRegister,
},
{
text: "📝 여행기 작성",
route: ROUTE_PATHS_MAP.travelogueRegister,
},
];
Comment on lines +3 to +12

Choose a reason for hiding this comment

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

상수화 굳!


export default SUB_BUTTONS;
97 changes: 97 additions & 0 deletions frontend/src/components/common/FocusTrap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, { useEffect, useRef } from "react";

const getFocusableElements = (element: HTMLElement | null): HTMLElement[] => {
if (!element) return [];

const focusableSelectors = [
"a[href]",
"button",
"input",
"textarea",
"select",
'[tabindex]:not([tabindex="-1"])',
"li",
].join(",");

return Array.from(element.querySelectorAll<HTMLElement>(focusableSelectors)).filter(
(el) => !el.hasAttribute("disabled") && !el.getAttribute("aria-hidden"),
);
};

interface Props extends React.ComponentPropsWithoutRef<"div"> {
children: React.ReactElement;
onEscapeFocusTrap?: () => void;
}

const FocusTrap = <T extends HTMLElement>({ children, onEscapeFocusTrap, ...props }: Props) => {
const focusTrapRef = useRef<T>(null);
const child = React.Children.only(children);
const focusableElements = useRef<(HTMLElement | null)[]>([]);
const currentFocusIndex = useRef(-1);

useEffect(() => {
if (focusTrapRef.current) {
focusableElements.current = getFocusableElements(focusTrapRef.current);
}

return () => {
focusableElements.current = [];
};
}, []);

useEffect(() => {
const focusNextElement = () => {
currentFocusIndex.current =
(currentFocusIndex.current + 1) % focusableElements.current.length;
focusableElements.current[currentFocusIndex.current]?.focus();
};

const focusPrevElement = () => {
currentFocusIndex.current =
(currentFocusIndex.current - 1 + focusableElements.current.length) %
focusableElements.current.length;
focusableElements.current[currentFocusIndex.current]?.focus();
};

const handleTabKeyDown = (event: KeyboardEvent) => {
const isTabKeyDown = !event.shiftKey && event.key === "Tab";
if (!isTabKeyDown) return;

event.preventDefault();
focusNextElement();
};

const handleShiftTabKeyDown = (event: KeyboardEvent) => {
const isShiftTabKeyDown = event.shiftKey && event.key === "Tab";
if (!isShiftTabKeyDown) return;

event.preventDefault();
focusPrevElement();
};

const handleEscapeKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape" && onEscapeFocusTrap) {
onEscapeFocusTrap();
}
};

const handleKeyPress = (event: KeyboardEvent) => {
handleTabKeyDown(event);
handleShiftTabKeyDown(event);
onEscapeFocusTrap && handleEscapeKeyDown(event);
};

document.addEventListener("keydown", handleKeyPress);

return () => document.removeEventListener("keydown", handleKeyPress);
}, [onEscapeFocusTrap]);

const Component = React.cloneElement(child, {
...{ ...props, ...child?.props },
ref: focusTrapRef,
});

return <>{Component}</>;
};

export default FocusTrap;
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const DefaultHeader = () => {
iconType="home-icon"
size="20"
onClick={() => navigation(ROUTE_PATHS_MAP.root)}
aria-label="홈 이동"
/>
}
isHamburgerUsed
Expand Down
8 changes: 1 addition & 7 deletions frontend/src/components/common/Header/Header.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,7 @@ export const DrawHeaderContainer = styled.div`
display: flex;
`;

export const MenuItem = styled.li`
${(props) => props.theme.typography.mobile.bodyBold};
padding: ${({ theme }) => theme.spacing.s};
cursor: pointer;
`;

export const MenuList = styled.ul`
export const MenuList = styled.div`
display: flex;
flex-direction: column;
justify-content: space-between;
Expand Down
Loading