useRovingFocus
컨트롤 묶음에 탭 정지를 하나만 둡니다. Tab을 누르면 항목 하나로 묶음에 들어가고, 다음 Tab에서 묶음을 떠납니다. 방향 키, Home, End는 항목 사이를 이동합니다. Tabs와 Menu가 이 훅 위에 만들어졌고, 회사는 자체 툴바, 세그먼트 컨트롤, 카드 그리드에 이 훅을 씁니다.
묶음에는 getContainerProps()를, 각 항목에는 getItemProps(value, { disabled })를 펼치세요. 항목은 훅이 소유한 속성으로 DOM에서 찾으므로, 직접 만든 래퍼 안의 항목도 포함되며 순서는 문서 순서를 따릅니다. 비활성화된 항목은 건너뜁니다. 브라우저에는 아직 내장 포커스 그룹이 없으므로, 이 기능은 JavaScript에 남아 있습니다.
const roving = useRovingFocus({ orientation: 'horizontal', defaultActiveValue: 'bold' })
<div role="toolbar" aria-label="Text style" {...roving.getContainerProps()}>
<Button.Root {...roving.getItemProps('bold')}>Bold</Button.Root>
<Button.Root {...roving.getItemProps('italic')}>Italic</Button.Root>
</div>
orientation은 방향 키를 정합니다. vertical(기본값), horizontal, both 중 하나입니다. 컨테이너의 direction이 rtl이면 Left와 Right가 서로 바뀝니다. loop(기본값 true)는 끝에서 처음으로 돌아갑니다. typeahead는 입력한 글자로 시작하는 다음 항목으로 이동합니다. 항목의 텍스트가 레이블과 다르면 textValue를 넘기세요. tabStop: false는 모든 항목을 tabIndex={-1}로 만듭니다. 트리거에서 여는 메뉴처럼 코드로만 포커스가 들어가는 위젯에 씁니다. findItems(container)는 getItemProps 대신 항목을 찾습니다. Toolbar가 안에 있는 컨트롤을 이렇게 찾습니다. 이렇게 찾은 항목에는 값이 없으므로, 훅은 탭 정지를 두지 않고 항목 사이에서 포커스만 옮깁니다.
탭 정지는 activeValue를 따르며, 그 값이 null이거나 비활성화되었거나 렌더링되지 않았으면 활성화된 첫 항목으로 대체됩니다. 항목은 DOM에서 읽으므로, 서버에서 렌더링한 HTML에서는 activeValue나 defaultActiveValue가 가리키는 항목에만 탭 정지가 있습니다. 하이드레이션 전에도 묶음에 도달할 수 있어야 하면 둘 중 하나를 설정하세요. onActiveValueChange는 키, 클릭, 스크립트 중 무엇으로든 activeValue가 아닌 항목이 포커스를 받을 때마다 실행됩니다. 대체된 탭 정지도 포함되므로, 제어하는 쪽이 그 값을 유지할 수 있습니다. 네이티브 disabled 항목은 { disabled: true }를 넘긴 항목처럼 건너뜁니다. Shift와 방향 키, Home, End의 조합은 직접 처리합니다. 예를 들어 선택을 넓힐 때 씁니다. focus(value), focusFirst(), focusLast()는 직접 만든 핸들러에서 포커스를 옮기고, getItemNode(value)는 항목의 요소를 찾고, values()는 활성화된 값을 순서대로 읽고, reachableItems()는 지금 포커스가 닿을 수 있는 활성화된 항목을 반환합니다.
훅은 역할을 설정하지 않습니다. 컨테이너에는 toolbar, tablist, menu, radiogroup처럼 위젯에 필요한 역할을 주세요.
언제 쓰나요
언제: 탭 정지 하나와 방향 키가 기대되는 복합 위젯. 툴바, 세그먼트 컨트롤, 탭 목록, 메뉴, 카드 그리드.
쓰지 않을 때: 각 컨트롤이 각자 탭 정지인 폼이나 링크 목록. 입력에 포커스를 유지하는 listbox → OptionList와 Combobox 레시피처럼 aria-activedescendant.
Props
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
| orientation | "horizontal" | "vertical" | "both" | 아니요 | "vertical" | 포커스를 옮기는 화살표 키. 오른쪽에서 왼쪽으로 쓰는(RTL) 컨테이너에서는 Left와 Right가 바뀝니다. |
| loop | boolean | 아니요 | true | 마지막 항목에서 처음으로, 처음에서 마지막으로 순환합니다. |
| typeahead | boolean | 아니요 | false | 입력하면 입력한 문자로 텍스트가 시작하는 다음 항목에 포커스합니다. |
| tabStop | boolean | 아니요 | true | 항목 하나를 탭으로 이동 가능하게 유지합니다. 메뉴처럼 포커스가 코드로만 들어올 때는 false이며, 모든 항목이 tabIndex -1을 받습니다. |
| findItems | (container: HTMLElement) => HTMLElement[] | 아니요 | — | getItemProps 대신 활성 여부와 관계없이 항목을 찾습니다. 이런 항목에는 값이 없으므로 탭 정지를 두지 않고, 훅은 포커스만 옮깁니다. |
| activeValue | string | null | 아니요 | — | 제어 탭 정지점. null이거나 비활성화되었거나 없으면 첫 번째 활성 항목으로 대체됩니다. |
| defaultActiveValue | string | null | 아니요 | — | 비제어일 때의 초기 탭 정지점. |
| onActiveValueChange | (value: string) => void | 아니요 | — | 대체 탭 정지점을 포함해 activeValue가 아닌 항목이 포커스를 받으면 호출되므로, 제어하는 쪽이 이를 고정할 수 있습니다. |