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

[접근성개선] 스크린 리더가 태그 등록 메시지 안내 #727

Merged
merged 5 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 21 additions & 0 deletions frontend/src/components/ScreenReaderOnly/ScreenReaderOnly.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const ScreenReaderOnly = () => (
<div
id='screen-reader'
aria-live='polite'
aria-hidden='true'
style={{
position: 'absolute',
width: '1px',
height: '1px',
margin: '-1px',
padding: '0',
border: '0',
overflow: 'hidden',
clip: 'rect(0, 0, 0, 0)',
whiteSpace: 'nowrap',
wordWrap: 'normal',
}}
/>
);

export default ScreenReaderOnly;
10 changes: 4 additions & 6 deletions frontend/src/components/TagInput/TagInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ChangeEvent, Dispatch, KeyboardEvent, SetStateAction } from 'react';

import { Flex, Input, TagButton, Text } from '@/components';
import { ToastContext } from '@/contexts';
import { useCustomContext } from '@/hooks';
import { useCustomContext, useScreenReader } from '@/hooks';
import { validateTagLength } from '@/service/validates';
import { theme } from '@/style/theme';

Expand All @@ -16,6 +16,7 @@ interface Props {

const TagInput = ({ value, handleValue, resetValue, tags, setTags }: Props) => {
const { failAlert } = useCustomContext(ToastContext);
const { updateScreenReaderMessage } = useScreenReader();

const handleSpaceBarAndEnterKeydown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === ' ' || e.key === 'Enter') {
Expand All @@ -26,15 +27,12 @@ const TagInput = ({ value, handleValue, resetValue, tags, setTags }: Props) => {
};

const addTag = () => {
if (value === '') {
return;
}

if (tags.includes(value)) {
if (value === '' || tags.includes(value)) {
return;
}

setTags((prev) => [...prev, value]);
updateScreenReaderMessage(`${value} 태그 등록`);
};

const handleTagInput = (e: ChangeEvent<HTMLInputElement>) => {
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ export { default as NoSearchResults } from './NoSearchResults/NoSearchResults';
// Skeleton UI
export { default as LoadingBall } from './LoadingBall/LoadingBall';
export { default as LoadingFallback } from './LoadingFallback/LoadingFallback';

// ScreenReader
export { default as ScreenReaderOnly } from './ScreenReaderOnly/ScreenReaderOnly';
1 change: 1 addition & 0 deletions frontend/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { useDropdown } from './useDropdown';
export { useHeaderHeight } from './useHeaderHeight';
export { useInput } from './useInput';
export { useInputWithValidate } from './useInputWithValidate';
export { useScreenReader } from './useScreenReader';
export { useScrollToTargetElement } from './useScrollToTargetElement';
export { useWindowWidth } from './useWindowWidth';
export { useToggle } from './useToggle';
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/hooks/useScreenReader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useCallback } from 'react';

export const useScreenReader = () => {
const updateScreenReaderMessage = useCallback((message: string) => {
const element = document.getElementById('screen-reader');
Copy link
Contributor

Choose a reason for hiding this comment

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

한 페이지에 screen-reader 라는 id를 가진 DOM요소가 한 개 이상 있다면 메시지가 중복으로 읽히지 않을까라는 우려가 들긴 하네요..! 혹시 해당 부분 테스트가 완료 되었나요?!

Copy link
Contributor Author

@vi-wolhwa vi-wolhwa Oct 7, 2024

Choose a reason for hiding this comment

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

한 화면에 동일한 id를 갖는 요소는 존재할 수 없으니, 개발자의 실수로 생성했다면 에러가 나지 않을까 생각합니당

Copy link
Contributor

Choose a reason for hiding this comment

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

document.getElementById() 메소드는 첫 번째로 찾은 요소만 반환하기 때문에 에러가 날 일은 없을 듯 싶습니다..! 다만 원하는대로 동작하게 하기 위해서는 올바르게 <ScreenReader /> 를 찾을 수 있도록 다른 곳에서 id 사용에 유의해야겠네요..!!

덧붙여 혹시 조금 더 리액트스럽게 ref 를 사용해보는 방향은 어떨까요?


if (element) {
element.setAttribute('aria-hidden', 'false');
element.textContent = message;

setTimeout(() => {
element.setAttribute('aria-hidden', 'true');
element.textContent = '';
});
}
}, []);

return { updateScreenReaderMessage };
};
2 changes: 2 additions & 0 deletions frontend/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { RouterProvider } from 'react-router-dom';

import { AuthProvider, HeaderProvider, ToastProvider } from '@/contexts';

import { ScreenReaderOnly } from './components/index';
import router from './routes/router';
import GlobalStyles from './style/GlobalStyles';
import { theme } from './style/theme';
Expand Down Expand Up @@ -49,6 +50,7 @@ enableMocking().then(() => {
<HeaderProvider>
<GlobalStyles />
<RouterProvider router={router} />
<ScreenReaderOnly />
</HeaderProvider>
</ToastProvider>
</AuthProvider>
Expand Down