From bb91b6aafb3482563a5a0a9f6e3f6c4ca90b0071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=94=ED=95=98?= Date: Wed, 2 Oct 2024 17:05:47 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat(ScreenReaderScript):=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=B0=20=EB=A6=AC=EB=8D=94=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ScreenReaderScript/ScreenReaderScript.tsx | 43 +++++++++++++++++++ frontend/src/components/index.ts | 1 + 2 files changed, 44 insertions(+) create mode 100644 frontend/src/components/ScreenReaderScript/ScreenReaderScript.tsx diff --git a/frontend/src/components/ScreenReaderScript/ScreenReaderScript.tsx b/frontend/src/components/ScreenReaderScript/ScreenReaderScript.tsx new file mode 100644 index 000000000..c67c10f6c --- /dev/null +++ b/frontend/src/components/ScreenReaderScript/ScreenReaderScript.tsx @@ -0,0 +1,43 @@ +import { useEffect, useRef, useState } from 'react'; + +interface Props { + message: string; +} + +const ScreenReaderScript = ({ message }: Props) => { + const announcerRef = useRef(null); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + if (announcerRef.current) { + announcerRef.current.textContent = message; + } + + setIsVisible(true); + + const timer = setTimeout(() => { + setIsVisible(false); + if (announcerRef.current) { + announcerRef.current.textContent = ''; + } + }); + + return () => clearTimeout(timer); + }, [message]); + + return ( +
+ ); +}; + +export default ScreenReaderScript; diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index 34e350865..1c3237774 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -10,6 +10,7 @@ export { default as LikeCounter } from './LikeCounter/LikeCounter'; export { default as Modal } from './Modal/Modal'; export { default as NonmemberAlerter } from './NonmemberAlerter/NonmemberAlerter'; export { default as PagingButtons } from './PagingButtons/PagingButtons'; +export { default as ScreenReaderScript } from './ScreenReaderScript/ScreenReaderScript'; export { default as SelectList } from './SelectList/SelectList'; export { default as SourceCode } from './SourceCode/SourceCode'; export { default as SourceCodeEditor } from './SourceCodeEditor/SourceCodeEditor'; From fcd4aa763212ff8c69f156099c9cc87a968b8d9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=94=ED=95=98?= Date: Wed, 2 Oct 2024 17:06:12 +0900 Subject: [PATCH 2/5] =?UTF-8?q?refactor(TagInput):=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=B0=EB=A6=AC=EB=8D=94=EA=B0=80=20=ED=83=9C=EA=B7=B8=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EB=A9=94=EC=8B=9C=EC=A7=80=EB=A5=BC=20?= =?UTF-8?q?=EC=9D=BD=EB=8F=84=EB=A1=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/TagInput/TagInput.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/TagInput/TagInput.tsx b/frontend/src/components/TagInput/TagInput.tsx index 88440ec09..4f22d2543 100644 --- a/frontend/src/components/TagInput/TagInput.tsx +++ b/frontend/src/components/TagInput/TagInput.tsx @@ -1,6 +1,6 @@ -import { ChangeEvent, Dispatch, KeyboardEvent, SetStateAction } from 'react'; +import { ChangeEvent, Dispatch, KeyboardEvent, SetStateAction, useState } from 'react'; -import { Flex, Input, TagButton, Text } from '@/components'; +import { Flex, Input, TagButton, Text, ScreenReaderScript } from '@/components'; import { ToastContext } from '@/contexts'; import { useCustomContext } from '@/hooks'; import { validateTagLength } from '@/service/validates'; @@ -16,6 +16,7 @@ interface Props { const TagInput = ({ value, handleValue, resetValue, tags, setTags }: Props) => { const { failAlert } = useCustomContext(ToastContext); + const [screenReaderMessage, setScreenReaderMessage] = useState(''); const handleSpaceBarAndEnterKeydown = (e: KeyboardEvent) => { if (e.key === ' ' || e.key === 'Enter') { @@ -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]); + setScreenReaderMessage(`${value} 태그 등록`); }; const handleTagInput = (e: ChangeEvent) => { @@ -91,6 +89,8 @@ const TagInput = ({ value, handleValue, resetValue, tags, setTags }: Props) => { }} /> + + ); }; From df0f3e65f0bafad47b6a0bd48f6e19cfbf746568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=94=ED=95=98?= Date: Mon, 7 Oct 2024 15:36:32 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor(src):=20ScreenReader=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EC=9D=84=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=9B=85=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ScreenReaderScript/ScreenReaderScript.tsx | 43 ------------------- frontend/src/components/TagInput/TagInput.tsx | 12 +++--- frontend/src/components/index.ts | 1 - frontend/src/hooks/index.ts | 1 + frontend/src/hooks/useScreenReader.ts | 36 ++++++++++++++++ 5 files changed, 43 insertions(+), 50 deletions(-) delete mode 100644 frontend/src/components/ScreenReaderScript/ScreenReaderScript.tsx create mode 100644 frontend/src/hooks/useScreenReader.ts diff --git a/frontend/src/components/ScreenReaderScript/ScreenReaderScript.tsx b/frontend/src/components/ScreenReaderScript/ScreenReaderScript.tsx deleted file mode 100644 index c67c10f6c..000000000 --- a/frontend/src/components/ScreenReaderScript/ScreenReaderScript.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; - -interface Props { - message: string; -} - -const ScreenReaderScript = ({ message }: Props) => { - const announcerRef = useRef(null); - const [isVisible, setIsVisible] = useState(false); - - useEffect(() => { - if (announcerRef.current) { - announcerRef.current.textContent = message; - } - - setIsVisible(true); - - const timer = setTimeout(() => { - setIsVisible(false); - if (announcerRef.current) { - announcerRef.current.textContent = ''; - } - }); - - return () => clearTimeout(timer); - }, [message]); - - return ( -
- ); -}; - -export default ScreenReaderScript; diff --git a/frontend/src/components/TagInput/TagInput.tsx b/frontend/src/components/TagInput/TagInput.tsx index 4f22d2543..f8587893c 100644 --- a/frontend/src/components/TagInput/TagInput.tsx +++ b/frontend/src/components/TagInput/TagInput.tsx @@ -1,8 +1,8 @@ -import { ChangeEvent, Dispatch, KeyboardEvent, SetStateAction, useState } from 'react'; +import { ChangeEvent, Dispatch, KeyboardEvent, SetStateAction } from 'react'; -import { Flex, Input, TagButton, Text, ScreenReaderScript } from '@/components'; +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'; @@ -16,7 +16,7 @@ interface Props { const TagInput = ({ value, handleValue, resetValue, tags, setTags }: Props) => { const { failAlert } = useCustomContext(ToastContext); - const [screenReaderMessage, setScreenReaderMessage] = useState(''); + const { visuallyHiddenProps, updateScreenReaderMessage } = useScreenReader(); const handleSpaceBarAndEnterKeydown = (e: KeyboardEvent) => { if (e.key === ' ' || e.key === 'Enter') { @@ -32,7 +32,7 @@ const TagInput = ({ value, handleValue, resetValue, tags, setTags }: Props) => { } setTags((prev) => [...prev, value]); - setScreenReaderMessage(`${value} 태그 등록`); + updateScreenReaderMessage(`${value} 태그 등록`); }; const handleTagInput = (e: ChangeEvent) => { @@ -90,7 +90,7 @@ const TagInput = ({ value, handleValue, resetValue, tags, setTags }: Props) => { /> - +
); }; diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index 1c3237774..34e350865 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -10,7 +10,6 @@ export { default as LikeCounter } from './LikeCounter/LikeCounter'; export { default as Modal } from './Modal/Modal'; export { default as NonmemberAlerter } from './NonmemberAlerter/NonmemberAlerter'; export { default as PagingButtons } from './PagingButtons/PagingButtons'; -export { default as ScreenReaderScript } from './ScreenReaderScript/ScreenReaderScript'; export { default as SelectList } from './SelectList/SelectList'; export { default as SourceCode } from './SourceCode/SourceCode'; export { default as SourceCodeEditor } from './SourceCodeEditor/SourceCodeEditor'; diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index ed57bfe1f..bebf16a1b 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -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'; diff --git a/frontend/src/hooks/useScreenReader.ts b/frontend/src/hooks/useScreenReader.ts new file mode 100644 index 000000000..b57a92d67 --- /dev/null +++ b/frontend/src/hooks/useScreenReader.ts @@ -0,0 +1,36 @@ +import { useMemo, useRef, CSSProperties } from 'react'; + +export const useScreenReader = () => { + const elementRef = useRef(null); + + const updateScreenReaderMessage = (message: string) => { + if (elementRef.current) { + elementRef.current.textContent = message; + } + }; + + const visuallyHiddenStyle: CSSProperties = { + position: 'absolute', + width: '1px', + height: '1px', + margin: '-1px', + padding: '0', + border: '0', + overflow: 'hidden', + clip: 'rect(0, 0, 0, 0)', + whiteSpace: 'nowrap', + wordWrap: 'normal', + }; + + const visuallyHiddenProps = useMemo( + () => ({ + ref: elementRef, + style: visuallyHiddenStyle, + 'aria-live': 'polite' as const, + 'aria-hidden': false, + }), + [], + ); + + return { visuallyHiddenProps, updateScreenReaderMessage }; +}; From bbf8693db9151b62b14634325e9a27baa0406eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=94=ED=95=98?= Date: Mon, 7 Oct 2024 16:07:05 +0900 Subject: [PATCH 4/5] =?UTF-8?q?refactor(src):=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=B0=EB=A6=AC=EB=8D=94=20=EC=9A=94=EC=86=8C=EB=A5=BC=20Rou?= =?UTF-8?q?terProvider=20=EB=B0=96=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ScreenReader/ScreenReader.tsx | 21 +++++++++ frontend/src/components/TagInput/TagInput.tsx | 4 +- frontend/src/components/index.ts | 3 ++ frontend/src/hooks/useScreenReader.ts | 43 ++++++------------- frontend/src/index.tsx | 2 + 5 files changed, 40 insertions(+), 33 deletions(-) create mode 100644 frontend/src/components/ScreenReader/ScreenReader.tsx diff --git a/frontend/src/components/ScreenReader/ScreenReader.tsx b/frontend/src/components/ScreenReader/ScreenReader.tsx new file mode 100644 index 000000000..adc405c2c --- /dev/null +++ b/frontend/src/components/ScreenReader/ScreenReader.tsx @@ -0,0 +1,21 @@ +const ScreenReader = () => ( +