HTML 요소 포커스
HTML에서 <button>, <input>, <select>, <a>와 같은 요소는 대화형 요소는 기본적으로 포커싱이 된다.
그러나 대화형 요소가 아닌 비대화형 요소(div, td, ul, li, span, p...)는 포커싱이 발생하지 않는다.
즉, onFocus와 onBlur 이벤트가 발생하지 않는다.
비대화형 요소에 onFocus, onBlur 이벤트를 발생시키고 싶으면 2가지 방법이 있다.
1. tabIndex 속성 활용
2. 직접 마우스 이벤트 제어
직접 하나씩 알아보자
tabIndex 속성
tabIndex 속성은 요소의 종류와는 무관하게 포커스 가능함을 나타내는 속성이다.
Tab 키를 사용하는 연속적인 키보드 탐색(tab 탐색)에서 어느 순서에 위치할 지 지정한다.
tabIndex=-1로 표현되는 음의 정수 값은 tab 탐색으로 접근할 수는 없으나 자바스크립트나 시각적(마우스 클릭)으로는 포커스 가능한 것을 나타낸다.
대화형요소에 tabIndex=-1을 줌으로써, focus를 방지하거나, 비대화형 요소에 자바스크립트를 통한 focus를 유발하는 등의 방식으로 사용된다.
tabIndex=0
대화형 요소의 기본 tabIndex 값으로, tabIndex가 양수인 요소의 tab 탐색이 전부 끝난 뒤, focus를 받게 된다.
tabIndex= 양수
순서에 따른 focus
예시
만약 다음과 같이 tabIndex를 통해 focus를 주었다고 생각해보자.
import { useEffect, useRef, useState } from 'react';
import UserInstrutment from './UserInstrument';
interface UserTableContentInterface {
keyName: string;
value: string[] | string | boolean | null;
index: number;
}
function UserTableContent({ keyName, value, index }: UserTableContentInterface) {
const [isEdit, setIsEdit] = useState(false);
const tdRef = useRef<HTMLTableCellElement>(null);
function handleDoubleClick() {
tdRef.current?.focus();
setIsEdit(true);
}
function handleClickOutSide() {
setIsEdit(false);
}
useEffect(() => {
console.log(isEdit);
}, [isEdit]);
useEffect(() => {
console.log(document.activeElemnt);
}, [document.activeElement]);
return (
<td
ref={tdRef}
key={`${keyName}${value}array${index}`}
onDoubleClick={handleDoubleClick}
tabIndex={0}
onBlur={handleClickOutSide}>
{isEdit ? (
<UserInstrutment />
) : (
<>
{typeof value === 'object' &&
value?.map((item) => {
return <p>{item}</p>;
})}
</>
)}
</td>
);
}
export default UserTableContent;
const UserInstrutmentData = [
{
text: '보컬',
value: 'vocal',
},
{
text: '기타',
value: 'guitar',
},
{
text: '드럼',
value: 'drum',
},
{
text: '베이스',
value: 'base',
},
{
text: '건반',
value: 'piano',
},
{
text: '관객',
value: 'audience',
},
];
function UserInstrutment() {
return (
<ul>
{UserInstrutmentData.map((instItem, index) => {
return (
<li key={`${instItem.text}asds${index}adas`}>
<label htmlFor={instItem.value}>
<input type="checkbox" id={instItem.value} value={instItem.value}></input>
{instItem.text}
</label>
</li>
);
})}
</ul>
);
}
export default UserInstrutment;
<td> 태그가 비대화형 요소이기에 tabIndex로 focus를 준 것이다.
그런데, 키보드이벤트로 자식요소를 클릭하면 focus가 제대로 유지되지 않는다.
(설명이 빈약하여 영상으로 대체할까 싶었지만 pass)
그렇기때문에 2번째 방법으로 직접 마우스 이벤트를 제어해보자.
직접 마우스 이벤트 제어
이벤트 핸들러를 직접 추가하는 방식으로 마우스 이벤트 제어하는 커스텀 훅을 만들었다.
import { useEffect } from 'react';
interface handleClickInterface<T extends HTMLElement> {
ref: React.RefObject<T>;
id: string;
setEditEnd: () => void;
}
function useHandleClickOutSide<T extends HTMLElement>({
ref,
id,
setEditEnd,
}: handleClickInterface<T>) {
useEffect(() => {
function handleOutsideClick(event: MouseEvent) {
if (ref.current && !ref.current.contains(event.target as Node)) {
const target = event.target as HTMLElement;
const parent = target.closest(`#${id}`);
if (!parent) {
setEditEnd();
}
}
}
document.addEventListener('mousedown', handleOutsideClick);
return () => {
document.removeEventListener('mousedown', handleOutsideClick);
};
}, [ref, id, setEditEnd]);
}
export default useHandleClickOutSide;
커스텀 훅을 사용하는 컴포넌트에서도 비교적 간단하게 활용할 수 있었다.
import useHandleClickOutSide from '@/hooks/admin/useHandleClickOutside';
import { RefCallback, useCallback, useRef, useState } from 'react';
import { UserTableContentProps } from './UserTableContent';
const UserInstrutmentData = [
{
text: '보컬',
value: 'vocal',
},
{
text: '기타',
value: 'guitar',
},
{
text: '드럼',
value: 'drum',
},
{
text: '베이스',
value: 'base',
},
{
text: '건반',
value: 'piano',
},
{
text: '관객',
value: 'audience',
},
];
function TableDataSession({
keyName,
value,
handleRowDataChange,
}: UserTableContentProps) {
const ref = useRef<HTMLUListElement>(null);
const inputRef = useRef<HTMLInputElement[] | null[]>([]);
const [isEdit, setIsEdit] = useState<boolean>(false);
useHandleClickOutSide({
ref,
id: 'session-list',
setEditEnd: setHandleDataEnd,
});
function handleDoubleClick() {
setIsEdit(true);
}
function setHandleDataEnd() {
const nextValue: Array<string> = [];
inputRef.current.map((item) => {
if (item?.checked) {
nextValue.push(item.name);
}
});
setIsEdit(false);
handleRowDataChange({ keyName, value: nextValue });
}
const callbackRef = useCallback(
(node: HTMLInputElement | null, index: number) => {
if (node && inputRef.current) {
inputRef.current[index] = node;
if (typeof value === 'object') {
value.map((item) => {
item === inputRef.current[index]?.name
? (inputRef.current[index].checked = true)
: null;
});
}
}
},
[],
);
return (
<td key={`${keyName}${value}array`} onDoubleClick={handleDoubleClick}>
{isEdit ? (
<ul id="session-list" ref={ref}>
{UserInstrutmentData.map((instItem, index) => {
return (
<li key={`${instItem.text}asds${index}adas`}>
<label htmlFor={instItem.value}>
<input
type="checkbox"
id={instItem.value}
name={instItem.text}
ref={(node) => callbackRef(node, index)}></input>
{instItem.text}
</label>
</li>
);
})}
</ul>
) : (
<>
{typeof value === 'object' &&
value?.map((item) => {
if (typeof item === 'string')
return <p key={`session-or-team${item}${keyName}`}>{item}</p>;
else return <p key={`session${item}${keyName}`}>{item.name}</p>;
})}
</>
)}
</td>
);
}
export default TableDataSession;
시연 영상
참고 자료
'React 관련 > 프로젝트' 카테고리의 다른 글
[React] figma 디자인 적용하기 (1) | 2024.09.11 |
---|---|
[React] 우리집 고양이도 할 수 있는 카카오 로그인 구현(with firebase OAuth 2.0 소셜 로그인) (2) | 2024.08.12 |
[React] 구글 로그인 기능 구현(with firebase 소셜 로그인 인증) (0) | 2024.08.07 |
[React-Redux] 실제 프로젝트에 적용하고, thunk로 비동기 통신 구현 (0) | 2024.05.08 |
React 구글맵(googleMap) 최적화(with typescript) (2) | 2024.04.26 |