@@ -21,6 +21,13 @@ type RovingFocusRootBaseProps = {
21
21
* @default false
22
22
*/
23
23
asChild ?: boolean ;
24
+ /**
25
+ * Changes what arrow keys are used to navigate the roving focus.
26
+ * Sets correct `aria-orientation` attribute, if `vertical` or `horizontal`.
27
+ *
28
+ * @default 'horizontal'
29
+ */
30
+ orientation ?: 'vertical' | 'horizontal' | 'ambiguous' ;
24
31
} & HTMLAttributes < HTMLElement > ;
25
32
26
33
export type RovingFocusElement = {
@@ -34,6 +41,7 @@ export type RovingFocusProps = {
34
41
setFocusableValue : ( value : string ) => void ;
35
42
focusableValue : string | null ;
36
43
onShiftTab : ( ) => void ;
44
+ orientation : 'vertical' | 'horizontal' | 'ambiguous' ;
37
45
} ;
38
46
39
47
export const RovingFocusContext = createContext < RovingFocusProps > ( {
@@ -46,76 +54,91 @@ export const RovingFocusContext = createContext<RovingFocusProps>({
46
54
/* intentionally empty */
47
55
} ,
48
56
focusableValue : null ,
57
+ orientation : 'horizontal' ,
49
58
} ) ;
50
59
51
60
export const RovingFocusRoot = forwardRef <
52
61
HTMLElement ,
53
62
RovingFocusRootBaseProps
54
- > ( ( { activeValue, asChild, onBlur, onFocus, ...rest } , ref ) => {
55
- const Component = asChild ? Slot : 'div' ;
63
+ > (
64
+ (
65
+ {
66
+ activeValue,
67
+ asChild,
68
+ orientation = 'horizontal' ,
69
+ onBlur,
70
+ onFocus,
71
+ ...rest
72
+ } ,
73
+ ref ,
74
+ ) => {
75
+ const Component = asChild ? Slot : 'div' ;
56
76
57
- const [ focusableValue , setFocusableValue ] = useState < string | null > ( null ) ;
58
- const [ isShiftTabbing , setIsShiftTabbing ] = useState ( false ) ;
59
- const elements = useRef ( new Map < string , HTMLElement > ( ) ) ;
60
- const myRef = useRef < HTMLElement > ( ) ;
77
+ const [ focusableValue , setFocusableValue ] = useState < string | null > ( null ) ;
78
+ const [ isShiftTabbing , setIsShiftTabbing ] = useState ( false ) ;
79
+ const elements = useRef ( new Map < string , HTMLElement > ( ) ) ;
80
+ const myRef = useRef < HTMLElement > ( ) ;
61
81
62
- const refs = useMergeRefs ( [ ref , myRef ] ) ;
82
+ const refs = useMergeRefs ( [ ref , myRef ] ) ;
63
83
64
- const getOrderedItems = ( ) : RovingFocusElement [ ] => {
65
- if ( ! myRef . current ) return [ ] ;
66
- const elementsFromDOM = Array . from (
67
- myRef . current . querySelectorAll < HTMLElement > (
68
- '[data-roving-tabindex-item]' ,
69
- ) ,
70
- ) ;
84
+ const getOrderedItems = ( ) : RovingFocusElement [ ] => {
85
+ if ( ! myRef . current ) return [ ] ;
86
+ const elementsFromDOM = Array . from (
87
+ myRef . current . querySelectorAll < HTMLElement > (
88
+ '[data-roving-tabindex-item]' ,
89
+ ) ,
90
+ ) ;
71
91
72
- return Array . from ( elements . current )
73
- . sort (
74
- ( a , b ) => elementsFromDOM . indexOf ( a [ 1 ] ) - elementsFromDOM . indexOf ( b [ 1 ] ) ,
75
- )
76
- . map ( ( [ value , element ] ) => ( { value, element } ) ) ;
77
- } ;
92
+ return Array . from ( elements . current )
93
+ . sort (
94
+ ( a , b ) =>
95
+ elementsFromDOM . indexOf ( a [ 1 ] ) - elementsFromDOM . indexOf ( b [ 1 ] ) ,
96
+ )
97
+ . map ( ( [ value , element ] ) => ( { value, element } ) ) ;
98
+ } ;
78
99
79
- useEffect ( ( ) => {
80
- setFocusableValue ( activeValue ?? null ) ;
81
- } , [ activeValue ] ) ;
100
+ useEffect ( ( ) => {
101
+ setFocusableValue ( activeValue ?? null ) ;
102
+ } , [ activeValue ] ) ;
82
103
83
- return (
84
- < RovingFocusContext . Provider
85
- value = { {
86
- elements,
87
- getOrderedItems,
88
- focusableValue,
89
- setFocusableValue,
90
- onShiftTab : ( ) => {
91
- setIsShiftTabbing ( true ) ;
92
- } ,
93
- } }
94
- >
95
- < Component
96
- { ...rest }
97
- tabIndex = { isShiftTabbing ? - 1 : 0 }
98
- onBlur = { ( e : FocusEvent < HTMLElement > ) => {
99
- onBlur ?.( e ) ;
100
- setIsShiftTabbing ( false ) ;
101
- setFocusableValue ( activeValue ?? null ) ;
104
+ return (
105
+ < RovingFocusContext . Provider
106
+ value = { {
107
+ elements,
108
+ getOrderedItems,
109
+ focusableValue,
110
+ setFocusableValue,
111
+ onShiftTab : ( ) => {
112
+ setIsShiftTabbing ( true ) ;
113
+ } ,
114
+ orientation,
102
115
} }
103
- onFocus = { ( e : FocusEvent < HTMLElement > ) => {
104
- onFocus ?.( e ) ;
105
- if ( e . target !== e . currentTarget ) return ;
106
- const orderedItems = getOrderedItems ( ) ;
107
- if ( orderedItems . length === 0 ) return ;
116
+ >
117
+ < Component
118
+ { ...rest }
119
+ tabIndex = { isShiftTabbing ? - 1 : 0 }
120
+ onBlur = { ( e : FocusEvent < HTMLElement > ) => {
121
+ onBlur ?.( e ) ;
122
+ setIsShiftTabbing ( false ) ;
123
+ setFocusableValue ( activeValue ?? null ) ;
124
+ } }
125
+ onFocus = { ( e : FocusEvent < HTMLElement > ) => {
126
+ onFocus ?.( e ) ;
127
+ if ( e . target !== e . currentTarget ) return ;
128
+ const orderedItems = getOrderedItems ( ) ;
129
+ if ( orderedItems . length === 0 ) return ;
108
130
109
- if ( focusableValue != null ) {
110
- elements . current . get ( focusableValue ) ?. focus ( ) ;
111
- } else if ( activeValue != null ) {
112
- elements . current . get ( activeValue ) ?. focus ( ) ;
113
- } else {
114
- orderedItems . at ( 0 ) ?. element . focus ( ) ;
115
- }
116
- } }
117
- ref = { refs }
118
- />
119
- </ RovingFocusContext . Provider >
120
- ) ;
121
- } ) ;
131
+ if ( focusableValue != null ) {
132
+ elements . current . get ( focusableValue ) ?. focus ( ) ;
133
+ } else if ( activeValue != null ) {
134
+ elements . current . get ( activeValue ) ?. focus ( ) ;
135
+ } else {
136
+ orderedItems . at ( 0 ) ?. element . focus ( ) ;
137
+ }
138
+ } }
139
+ ref = { refs }
140
+ />
141
+ </ RovingFocusContext . Provider >
142
+ ) ;
143
+ } ,
144
+ ) ;
0 commit comments