-
Notifications
You must be signed in to change notification settings - Fork 5
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
base: develop/fe
Are you sure you want to change the base?
Changes from all commits
53f180a
e46a62a
0d0b8d4
af6ffd4
28d74fd
1e70067
a82b9a9
6e32023
2de64e0
e490f5e
903206e
d507bfc
e37935c
1b6e35b
5a1edf1
a04e8dd
72ff036
1810ce1
dcd103a
d091c40
c24d50c
5931297
8a08609
735d281
5760fdd
67dad24
3a923ef
1b5e434
2e92906
81ba587
e8f4a4c
c92ada7
7ac5c26
623cadc
3e519ca
d3b4654
d998713
b524e4b
517a969
c5b0733
2db1cb2
2c4b741
85e2886
ee46e07
10a5943
f79737b
8d32894
8c5d737
3974935
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const DEFAULT_ELEMENT = "li" as const; |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. useToggle에 toggle 함수 만들고 해당 훅 이용하면 더 좋을 거 같아요! |
||
usePressESC(isOpen, toggleDrawer); | ||
|
||
useModalControl(isOpen, toggleDrawer); | ||
|
||
|
@@ -31,12 +34,19 @@ const Drawer = ({ children }: React.PropsWithChildren) => { | |
|
||
return ( | ||
<DrawerProvider isOpen={isOpen} toggleDrawer={toggleDrawer}> | ||
<VisuallyHidden aria-live="assertive"> | ||
{isOpen ? "사용자 메뉴 모달이 열렸습니다." : "사용자 메뉴 모달이 닫혔습니다."} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
); | ||
}; | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. interface TriggerProps extends PropsWithChildren<React.ButtonHTMLAttributes> { |
||
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> | ||
); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오오 color 바꿔주신부분 좋아요! 다만 해당 컬러는 gray[500]이고, text secondary는 gray[700]인걸로 파악했습니다. 혹시 이 컬러로 변경한 이유가 있을까요?! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아아 이 부분에서 색상 대조 이슈가 생기더구라욥 |
||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
|
@@ -19,34 +22,47 @@ const FloatingButton = () => { | |
setIsOpen((prev) => !prev); | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 얘도 플로팅 버튼을 직접적으로 쓰면 모를 거 같아요. 여행기 및 여행 계획 작성 메뉴가 열렸습니다 정도면 어떨까요? |
||
</VisuallyHidden> | ||
{isOpen && ( | ||
<> | ||
<S.BackdropLayout onClick={handleToggleButton} /> | ||
<FocusTrap> | ||
<S.SubButtonContainer $isOpen={isOpen}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
); | ||
|
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 상수화 굳! |
||
|
||
export default SUB_BUTTONS; |
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ImgHTMLAttributes로 alt나 className같은 속성들을 외부에서 받도록 처리했네요! 좋은거 같아요 👍👍👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋네요 저도 onLoad를 이용해야했는데 해당 방법으로 수정하니 유연하게 대응할 수 있었습니다 !