본문 바로가기
React 관련/프로젝트

[React] 비대화형 요소(td, div)에 마우스 이벤트(onFocus, onBlur) 넣기

by ash9river 2024. 9. 23.

 

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;

시연 영상

참고 자료

 

 

tabindex - HTML: Hypertext Markup Language | MDN

tabindex 전역 특성은 요소가 포커스 가능함을 나타내며, 이름에서도 알 수 있듯, 주로 Tab 키를 사용하는 연속적인 키보드 탐색에서 어느 순서에 위치할지 지정합니다.

developer.mozilla.org

 

 

🌐 HTML 포커스 제어하기 - focus, blur

HTML 포커스 제어 사진이나 이미지와 같은 작업을 다뤄보신 독자 분은 아마 focus와 blur 처리에 대해 들어본 적이 있을 것이다. 사진에 블러 처리를 하면 이미지가 흐려지고 포커스 처리하면 흐린

inpa.tistory.com

 

 

자바스크립트 onfocus 와 onblur 그리고 relatedTarget

HTML 요소 포커싱 HTLM 요소의 포커싱 관련 이벤트로 onfocus 와 onblur 가 있다. 이 두 이벤트는 서로 짝꿍이다. 이름 그대로 HTML 요소가 포커싱 되었을땐 onfocus 이벤트가 발생하고, 반대로 포커싱 해제

kwangsunny.tistory.com