[React] tab focusing: tabIndex, focus-visible

KoEunseo·2023년 1월 12일
0

project

목록 보기
28/37

탭으로 상호작용 할 수 있는 웹을 만들자.
마우스로 손을 옮기기 귀찮아서 탭을 쓴 경험이 다들 있을것이다.
무선마우스를 쓴다면 배터리가 닳아서 하는 수 없이 키보드로만 컴퓨터를 써야 할 때도 분명 있었다.

다른것도 시급하지만... 그래서 이부분을 먼저 리팩토링해보기로 했다.
마음먹은 거에 비해 그리 대단히 손이 가는 건 아니었다.
'tabindex'를 쓰면 된다.

tabindex: 요소가 포커스될 수 있음을 나타낸다.
tab 키를 연속적으로 눌렀을 때 어느 순서로 이동할지 설정해준다.

  • -1 : tab으로 접근할 수는 없지만 마우스 클릭으로는 포커스 가능. 위젯 접근성 확보를 위해 사용한다고 한다.
  • 양의 정수 : 순서를 값으로 지정한다. 최대 32767까지 지정 가능하다.
  • 0 : tab으로 포커스 받을 수 없는 요소. 주로 text
<input tabIndex={1} />

생각보다 적용하는데 시간이 많이 들었다.
input, button 등 포커스가 필요한 요소에는 기본적으로 tab 키를 눌렀을때 포커싱이 간다.

굳이 tabIndex를 다 줄 필요는 없어보인다.

하나하나 주려고 하다 보니 순서가 이상해졌다.
선택적으로 주는 것이 오히려 좋아보임.

오히려 tabIndex를 주기보다 tab을 사용해서 요소가 포커싱 되었을때 시각적으로 표현해 주는 것이 더 중요한 것 같다.

css 가상선택자 중에서 :focus 라는 것이 있는데, focus로 스타일링하면 tab했을때 스타일링이 되긴 하지만 마우스로 클릭했을때도 스타일링되버린다.
그럴때 쓰는것이 :focus-visible 이다.
마우스로 클릭했을때는 스타일링이 되지 않고 tab을 사용했을때 스타일이 적용된다.

(+) 앗, 그런데 버튼은 잘 작동되는데 비해 input은 마우스로 포커스를 주었을때도 스타일이 적용되는 것을 볼 수 있었다.

그런데 :focus-visible를 사용해보니 border가 생기기 때문에 레이아웃이 어느정도 무너진다.

그래서 나는 가상 클래스 선택자를 하나 더 사용했다.
바로 :after이다.
내가 가장 많이 사용해본 가상선택자 중 하나가 아닐까 싶은데,
아래와 같이 사용한다.
비슷하게 :before도 있다.

export const ButtonStyle = styled.button`
  position: relative;
  &:focus-visible::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 100%;
    height: 100%;
    border: 0.1rem solid #171e71;
    border-radius: 0.3rem;
  }
`;

가상클래스 선택자로 focus 주기

버튼을 기준으로(relative) 가상요소를 정렬한다.
transform과 top, left를 위와 같이 주어 가상요소의 한가운데를 기준으로 하도록 조정한다.
너비와 높이를 버튼만큼 차지하도록 한다.
그리고 content에 빈문자열을 꼭 넣어주어야 한다.
위의 과정을 다 거쳐야 가상요소가 화면에 나타난다.
하나라도 빠지면 안나옴.

그리고 이젠 내가 주고싶은 스타일을 추가하면 된다.
보통 포커스할때 하는것처럼 보더를 주었다.

가상클래스를 사용하면 기존의 레이아웃에 영향을 주지 않고 스타일링 할 수가 있다.

버튼으로 이벤트핸들러를 발생시킬 수 있도록 한다.

tabIndex를 todo item에 주었다.
투두는 버튼도 인풋도 아니라서 tabIndex를 주어야 tab으로 선택 가능하다.

<TodoListStyle.TodoUl>
      {
        todos.map((todo, i) => {
          return (
            <div key={todo.id}>
              <TodoItem key={todo.id} todo={todo} refresher={refresher} tabIndex={i+1}
            />
            </div>
          )
        })
      }
    </TodoListStyle.TodoUl>

이렇게 tabIndex에 i+1을 주었다. 0이 되면 안되고 1부터 시작해야하니까!
그리고 엔터를 쳐서 이벤트핸들러가 발생할 수 있도록 함수를 하나 더 만들었다.
onKeyPress로

  // 마우스로 동작하는 기존함수. onClick
  const handleOpenDetailPage = () => {
    curParams === todo.id ?
    navigate('/') :
    navigate(`${todo.id}`);
  };
  // 엔터키를 눌렀을 때 위 함수를 콜한다. onKeyPress
  const OpenDetailWithEnter = (e) => {
    if(e.key === 'Enter') {
      handleOpenDetailPage();
    }
  }
    return (
    <>
      <TodoItemStyle.TodoItemBox 
      key={todo.id} 
      onClick={handleOpenDetailPage} 
      onKeyPress={OpenDetailWithEnter} 
      tabIndex={tabIndex+1}>
        <TodoItemStyle.TodoTitle />
      </TodoItemStyle.TodoItemBox>
    </>
  )

이제 탭으로만 자유자재로 동작하는 투두가 되었다!! 후후😝

(+) 추가

리베하얀님 유튜브 쇼츠 보다가 좋은거 발견해서 추가해본다.
https://youtube.com/shorts/Geu8Un0CwMs?feature=share

.check:focus + label::before {
  outline: 20x dashed blue;
  outline-offset: 3px; //간격 벌리기도 가능
  outline-width: 12px; //px | thin | medium | thick 
  outline-style: none; | dotted | solid | groove | inset
}
//border-radious를 주면 둥글게도 할 수 있다.

check가 포커싱되면 인접한 라벨의 가상요소에 보더가 생긴다.

아웃라인은 처음 보는데 보더랑 무슨 차이가 있는 것일까??

border는 박스 사이즈에 px이 추가된다. 박스 크기가 늘어나는것!
그러나 아웃라인은 박스 사이즈가 라인을 주기 전과 동일하다.
왜 이게 중요하냐면... 레이아웃이 달라지기때문.

내가 위에서 보더를 주었기 때문에 레이아웃이 망가져 가상선택자를 사용해 아주 복잡한 방법으로 보완을 했다.
사실상 아웃라인이라는 좋은 방법이 있었음에도 불구하고.... ㅠㅠ 이제라도 알아서 아주 다행이다...!!!!

profile
주니어 플러터 개발자의 고군분투기

0개의 댓글