-
Notifications
You must be signed in to change notification settings - Fork 0
refactor/스크립트 뷰어 컴포넌트 #17
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
Changes from 9 commits
ea8dd3b
99c65a8
fde7700
c7cb045
9d135f1
7ad9fdf
b705152
8a1ee49
ad6d848
682d0c1
a054de7
06ada31
dbb1bc8
baaf388
2fe865a
9e45a5e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| import { type ReactNode, useCallback, useEffect, useId, useRef, useState } from 'react'; | ||
|
|
||
| import clsx from 'clsx'; | ||
|
|
||
| type PopoverPosition = 'top' | 'bottom'; | ||
| type PopoverAlign = 'start' | 'end'; | ||
|
|
||
| interface PopoverProps { | ||
| trigger: ReactNode | ((props: { isOpen: boolean }) => ReactNode); | ||
| children: ReactNode; | ||
| position?: PopoverPosition; | ||
| align?: PopoverAlign; | ||
| className?: string; | ||
| ariaLabel?: string; | ||
| } | ||
|
|
||
| export function Popover({ | ||
| trigger, | ||
| children, | ||
| position = 'top', | ||
| align = 'end', | ||
| className, | ||
| ariaLabel, | ||
| }: PopoverProps) { | ||
| const [isOpen, setIsOpen] = useState(false); | ||
| const popoverRef = useRef<HTMLDivElement>(null); | ||
| const triggerRef = useRef<HTMLDivElement>(null); | ||
| const contentRef = useRef<HTMLDivElement>(null); | ||
| const popoverId = useId(); | ||
|
|
||
| const handleToggle = useCallback(() => { | ||
| setIsOpen((prev) => !prev); | ||
| }, []); | ||
|
|
||
| const handleClose = useCallback(() => { | ||
| setIsOpen(false); | ||
| // 팝오버 닫힐 때 트리거로 포커스 이동 | ||
| triggerRef.current?.querySelector('button')?.focus(); | ||
| }, []); | ||
|
Comment on lines
+33
to
+54
Contributor
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. 팝오버가 닫힐 때 포커스를 복원하는 방식이 특정 구조( |
||
|
|
||
| // Escape 키로 닫기 | ||
| useEffect(() => { | ||
| if (!isOpen) return; | ||
|
|
||
| const handleKeyDown = (e: KeyboardEvent) => { | ||
| if (e.key === 'Escape') { | ||
| handleClose(); | ||
| } | ||
| }; | ||
|
|
||
| document.addEventListener('keydown', handleKeyDown); | ||
| return () => document.removeEventListener('keydown', handleKeyDown); | ||
| }, [isOpen, handleClose]); | ||
|
|
||
| // 외부 클릭 시 닫기 | ||
| useEffect(() => { | ||
| if (!isOpen) return; | ||
|
|
||
| const handleClickOutside = (e: MouseEvent) => { | ||
| if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) { | ||
| handleClose(); | ||
| } | ||
| }; | ||
|
|
||
| document.addEventListener('mousedown', handleClickOutside); | ||
| return () => document.removeEventListener('mousedown', handleClickOutside); | ||
| }, [isOpen, handleClose]); | ||
|
|
||
| // 팝오버 열릴 때 첫 번째 포커스 가능한 요소로 포커스 이동 | ||
| useEffect(() => { | ||
| if (!isOpen || !contentRef.current) return; | ||
|
|
||
| const focusableElements = contentRef.current.querySelectorAll<HTMLElement>( | ||
| 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', | ||
| ); | ||
|
|
||
| if (focusableElements.length > 0) { | ||
| focusableElements[0].focus(); | ||
| } | ||
| }, [isOpen]); | ||
|
Comment on lines
+84
to
+95
|
||
|
|
||
| const positionClasses = clsx({ | ||
| 'bottom-full mb-2': position === 'top', | ||
| 'top-full mt-2': position === 'bottom', | ||
| }); | ||
|
|
||
| const alignClasses = clsx({ | ||
| 'left-0': align === 'start', | ||
| 'right-0': align === 'end', | ||
| }); | ||
|
|
||
| return ( | ||
| <div ref={popoverRef} className="relative"> | ||
| <div | ||
| ref={triggerRef} | ||
| onClick={handleToggle} | ||
| aria-haspopup="dialog" | ||
| aria-expanded={isOpen} | ||
| aria-controls={isOpen ? popoverId : undefined} | ||
| > | ||
| {typeof trigger === 'function' ? trigger({ isOpen }) : trigger} | ||
| </div> | ||
|
Comment on lines
+109
to
+117
Contributor
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. 접근성 관점에서 이 문제를 해결하기 위해 |
||
|
|
||
| {isOpen && ( | ||
| <div | ||
| ref={contentRef} | ||
| id={popoverId} | ||
| role="dialog" | ||
| aria-label={ariaLabel} | ||
| aria-modal="false" | ||
|
Comment on lines
+123
to
+125
|
||
| className={clsx( | ||
| 'absolute z-50', | ||
| positionClasses, | ||
| alignClasses, | ||
| 'rounded-lg bg-white shadow-[0_0.25rem_1.25rem_rgba(0,0,0,0.05)]', | ||
| className, | ||
| )} | ||
| > | ||
| {children} | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| export { LoginButton } from './LoginButton'; | ||
| export { Logo } from './Logo'; | ||
| export { Popover } from './Popover'; |
Uh oh!
There was an error while loading. Please reload this page.