공간 탐색 (방향 키패드 포커스)

웹 기능: scrollIntoView()

방향 포커스 탐색 (화살표 키 또는 D패드 사용)을 사용하면 키보드 전용 및 대체 입력 사용자가 시각적 2D 레이아웃을 기반으로 대화형 항목을 탐색할 수 있습니다. 미디어 레일, 대시보드 패널, 중첩된 카탈로그와 같은 공간 사용자 인터페이스에 매우 유용하지만, 표시 영역 스크롤과 같은 네이티브 브라우저 동작을 방해하지 않도록 신중하게 구현해야 합니다.

이 가이드에서는 복합 위젯 내에서 성능이 우수하고 접근성이 뛰어난 2D 방향 탐색을 빌드하는 방법을 설명합니다.


핵심 아키텍처 원칙

맞춤 화살표 키 공간 포커스를 구현할 때는 다음 기본 규칙을 준수하세요.

  1. 복합 위젯으로 제한: 기본 순차 Tab 탐색이 예상되는 일반 문서 텍스트 플로우 또는 간단한 세로 목록 레이아웃에서 화살표 키를 재정의하지 않습니다. 일반적인 문서 흐름 안내는 접근성 코딩 가이드라인을 참고하세요.
  2. 기본 경계 스크롤 유지: 누른 방향에 포커스 가능한 요소가 없으면 기본 브라우저 동작을 방지하지 않습니다. 브라우저가 표시 영역을 기본적으로 스크롤할 수 있도록 이벤트를 버블링합니다. 화살표 키를 재정의하면 접근성이 무조건 중단됩니다 (WCAG 1.4.10).
  3. 포커스 가시성 및 정렬 보장: 포커스가 변경되면 새로 포커스가 지정된 요소가 완전히 표시되도록 뷰로 스크롤되는지 확인합니다 (예: scrollIntoView({ block: 'nearest', inline: 'nearest' }) 사용).
  4. 레이아웃 시스템과 조정: 각 키 누름을 처리하는 동안 실시간 경계 도형 (getBoundingClientRect())에서 포커스 후보 위치를 결정합니다. DOM 순서나 고정 그리드 구조가 시각적 레이아웃을 나타낸다고 가정하지 마세요. 최신 레이아웃 권장사항은 CSS 레이아웃 및 반응형 디자인을 참고하세요.

접근성 포커스 관리

공간 컨테이너는 순차적 페이지 탭 탐색에 단일 탭 정지를 노출해야 합니다. 위젯 내에서 Roving Tabindex Pattern을 사용하여 키보드 포커스를 관리합니다.

  • 활성 항목에 tabindex="0"가 있습니다.
  • 모든 비활성 항목에는 tabindex="-1"가 있습니다.
  • 탐색할 때 tabindex="0" 속성을 이동하고 focus()를 호출하여 프로그래매틱 방식으로 포커스를 이동합니다.

시각적 후보 선택

비대칭, 래핑 또는 그리드 레이아웃에서는 색인 기반 매핑이 실패합니다. 대신 현재 키 누르기의 실시간 시각적 도형에서 후보를 도출합니다.

  • 후보: checkVisibility()를 전달하는 위젯 컨테이너의 포커스 가능한 하위 요소를 사용합니다. 숨겨진 요소는 (0, 0)에서 크기가 0인 사각형을 가질 수 있으며 실수로 up 또는 left 타겟이 되어서는 안 됩니다.
  • Geometry: 키 누름을 처리하는 동안 포커스가 지정된 요소와 모든 후보의 getBoundingClientRect()를 읽습니다. 이러한 사각형을 전역적으로 캐시하지 마세요. 레이아웃은 여러 가지 이유로 변경될 수 있으며, 사용자 속도에 맞춘 키 이벤트를 처리할 때 소수의 도형 읽기는 비용이 적게 듭니다.
  • 방향 필터: 후보는 가까운 가장자리가 해당 축에서 현재 요소의 먼 가장자리에 있거나 그 너머에 있는 경우에만 눌린 방향에 있으며, 하위 픽셀 반올림에 대한 작은 허용 오차를 허용합니다. 예를 들어 right 후보에는 candidate.left >= current.right - tolerance가 있어야 합니다. 중심을 비교하여 후보가 방향에 있는지 확인하지 마세요.
  • 정렬된 후보 우선: 방향 필터를 통과한 후보 중 수직 축의 범위가 현재 요소의 범위와 겹치는 후보를 선호합니다. 탐색 축에서 가장 작은 간격을 선택하고 가장 작은 수직 중심 오프셋으로 동률을 해소합니다. 이렇게 하면 항목이 비정상적으로 넓은 경우에도 인접한 시각적 행 또는 열에서 탐색이 유지됩니다.
  • 대체 점수: 수직 축에서 겹치는 후보가 없으면 나머지 후보의 점수를 gap + w * perpendicularCentreOffset로 지정하고 최솟값을 선택합니다. 가중치 w >= 2을 시작점으로 사용한 다음 레이아웃에 맞게 조정합니다. 가중치가 클수록 더 가깝게 정렬된 후보가 선호됩니다.
  • 후보 없음: 아무것도 반환하지 않습니다. 이벤트 핸들러는 preventDefault()를 호출하지 않아야 하며, 브라우저가 기본적으로 스크롤할 수 있도록 허용해야 합니다.

정확한 구현은 다를 수 있지만 방향 테스트를 에지 기반으로 유지하고 가중치가 적용된 대체 전에 중복 환경설정을 적용합니다. 이것이 알고리즘의 핵심입니다. 캐시나 고정 가중치를 보편적으로 올바른 것으로 제시하지 마세요.


키 누르기 통합 및 스크롤 협력

이벤트 라우터는 유효한 공간 포커스 전환이 발생할 때만 표준 표시 영역 스크롤을 방지해야 합니다.

  • 특수키를 누르고 있거나, e.defaultPrevented가 이미 true이거나, 가장 안쪽의 이벤트 타겟이 화살표 키 자체를 사용하는 경우 이벤트를 가로채지 않습니다. e.target만 사용하지 말고 e.composedPath()[0]를 사용하세요. e.target는 shadow 호스트로 타겟팅을 변경할 수 있기 때문입니다. 실제 수정 가능/제어 검사는 target.matches(':read-write, select, input[type="range"], [role="slider"]')입니다. 닫힌 shadow root는 검사할 수 없습니다.
  • 여러 공간 위젯이 한 페이지에 공존할 수 있도록 리스너의 범위를 위젯 컨테이너로 지정합니다.
  • 포커스가 지정된 요소 자체가 후보인 경우에만 작동합니다. 항목에 포커스 가능한 자체 컨트롤이 포함된 경우 해당 컨트롤도 후보로 만들거나 그대로 둡니다. closest()로 상위 요소를 확인하지 마세요.
  • 후보가 발견되면 preventDefault()를 호출하고, 로빙 tabindex를 업데이트하고, focus()를 호출하고, scrollIntoView({ block: 'nearest', inline: 'nearest' })를 사용합니다.
  • 후보가 없으면 preventDefault()를 호출하지 않고 반환하여 네이티브 스크롤이 계속됩니다.

공간 탐색을 위해 h/j/k/l 또는 W/A/S/D와 같은 단일 문자 단축키를 추가하지 마세요. 화살표 키는 문자 키가 아니며, 후보가 없을 때 기본 스크롤을 유지하면 표시 영역 이동이 불필요하게 차단되는 것을 방지할 수 있습니다.