Skip to content

Commit cfe6a0b

Browse files
committed
feat: 버튼 컴포넌트를 forwardRef로 수정하고 모달에 포커스 관리 기능 추가
1 parent 2370c68 commit cfe6a0b

File tree

4 files changed

+73
-31
lines changed

4 files changed

+73
-31
lines changed

dev-dist/sw.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
7979
*/
8080
workbox.precacheAndRoute([{
8181
"url": "index.html",
82-
"revision": "0.2ufi9jpmuo"
82+
"revision": "0.1kau0siulq"
8383
}], {});
8484
workbox.cleanupOutdatedCaches();
8585
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

src/components/Button.tsx

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,40 @@
11
import { twMerge } from 'tailwind-merge';
2+
import { forwardRef } from 'react';
23

34
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
45
variant?: 'primary' | 'secondary' | 'disabled';
56
className?: string;
67
onClick?: (event?: React.MouseEvent<HTMLButtonElement>) => void;
78
}
89

9-
export default function Button({
10-
children,
11-
variant = 'primary',
12-
className,
13-
...props
14-
}: ButtonProps) {
15-
const buttonStyle = {
16-
primary: 'bg-primary-normal hover:bg-primary-hover',
17-
secondary: 'bg-white border border-primary-active text-primary-active hover:bg-gray-5',
18-
disabled: 'bg-gray-30 cursor-not-allowed',
19-
};
10+
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
11+
({ children, variant = 'primary', className, ...props }, ref) => {
12+
const buttonStyle = {
13+
primary: 'bg-primary-normal hover:bg-primary-hover',
14+
secondary: 'bg-white border border-primary-active text-primary-active hover:bg-gray-5',
15+
disabled: 'bg-gray-30 cursor-not-allowed',
16+
};
2017

21-
return (
22-
<>
23-
<button
24-
className={twMerge(
25-
'flex justify-center items-center w-full rounded-lg h-[38px] text-white transition body-m cursor-pointer',
26-
buttonStyle[variant],
27-
className,
28-
)}
29-
disabled={variant === 'disabled'}
30-
{...props}
31-
>
32-
{children}
33-
</button>
34-
</>
35-
);
36-
}
18+
return (
19+
<>
20+
<button
21+
ref={ref}
22+
className={twMerge(
23+
'flex justify-center items-center w-full rounded-lg h-[38px] text-white transition body-m cursor-pointer',
24+
buttonStyle[variant],
25+
className,
26+
)}
27+
disabled={variant === 'disabled'}
28+
{...props}
29+
>
30+
{children}
31+
</button>
32+
</>
33+
);
34+
},
35+
);
36+
37+
export default Button;
3738

3839
// 사용 예시
3940
// <Button variant="secondary" className="w-40 py-3 text-lg">

src/components/Modal.tsx

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,44 @@
11
import { useModalStore } from '@/store/modalStore';
22
import Button from './Button';
33
import { createPortal } from 'react-dom';
4+
import { useEffect, useRef } from 'react';
45
// import { spawn } from 'child_process';
56

67
export default function Modal() {
7-
const { modal } = useModalStore();
8+
const { modal, closeModal } = useModalStore();
9+
const confirmButtonRef = useRef<HTMLButtonElement | null>(null);
10+
const cancelButtonRef = useRef<HTMLButtonElement | null>(null);
11+
const modalRef = useRef<HTMLDivElement>(null);
12+
13+
useEffect(() => {
14+
if (modal.isOpen) {
15+
confirmButtonRef.current?.focus(); // 모달이 열리면 첫 번째 버튼에 포커스 이동
16+
}
17+
}, [modal.isOpen]);
18+
19+
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
20+
if (e.key === 'Escape') {
21+
closeModal();
22+
}
23+
24+
if (e.key === 'Tab') {
25+
e.preventDefault(); // 기본 Tab 동작 막기
26+
if (document.activeElement === confirmButtonRef.current) {
27+
cancelButtonRef.current?.focus(); // 확인 버튼 → 취소 버튼으로 이동
28+
} else {
29+
confirmButtonRef.current?.focus(); // 취소 버튼 → 확인 버튼으로 이동
30+
}
31+
}
32+
};
33+
834
if (!modal.isOpen) return null;
35+
936
return createPortal(
1037
<div
38+
ref={modalRef}
1139
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
1240
// onClick={closeModal}
41+
onKeyDown={(e) => handleKeyDown(e)}
1342
>
1443
<div
1544
className="bg-white p-5 mx-5 rounded-lg card-shadow w-[287px] min-h-[148px] flex flex-col justify-between"
@@ -26,11 +55,21 @@ export default function Modal() {
2655
</div>
2756
{modal.message && <p className="text-center caption-r">{modal.message}</p>}
2857
<div className="mt-4 flex justify-end gap-[6px]">
29-
<Button variant="primary" onClick={modal.onConfirm} className="body-m">
58+
<Button
59+
ref={confirmButtonRef}
60+
variant="primary"
61+
onClick={modal.onConfirm}
62+
className="body-m focus:bg-primary-active "
63+
>
3064
{modal.confirmText}
3165
</Button>
3266
{modal.onCancel && (
33-
<Button variant="secondary" onClick={modal.onCancel} className="body-m">
67+
<Button
68+
ref={cancelButtonRef}
69+
variant="secondary"
70+
onClick={modal.onCancel}
71+
className="body-m focus:bg-gray-5"
72+
>
3473
{modal.cancelText}
3574
</Button>
3675
)}

src/store/modalStore.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export const useModalStore = create<ModalStore>((set) => ({
2626
isOpen: false,
2727
title: '',
2828
message: '',
29+
confirmText: '확인',
30+
cancelText: '취소',
2931
onConfirm: undefined,
3032
onCancel: undefined,
3133
},

0 commit comments

Comments
 (0)