11// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22// SPDX-License-Identifier: Apache-2.0
33
4- import React , { useRef } from 'react' ;
5- import { render , act } from '@testing-library/react' ;
4+ import React , { useContext , useEffect , useRef } from 'react' ;
5+ import { render } from '@testing-library/react' ;
66
7- import { SingleTabStopNavigationContext , useSingleTabStopNavigation } from '../' ;
7+ import {
8+ SingleTabStopNavigationAPI ,
9+ SingleTabStopNavigationContext ,
10+ SingleTabStopNavigationProvider ,
11+ useSingleTabStopNavigation ,
12+ } from '../' ;
813import { renderWithSingleTabStopNavigation } from './utils' ;
914
15+ // Simple STSN subscriber component
1016function Button ( props : React . HTMLAttributes < HTMLButtonElement > ) {
1117 const buttonRef = useRef < HTMLButtonElement > ( null ) ;
1218 const { tabIndex } = useSingleTabStopNavigation ( buttonRef , { tabIndex : props . tabIndex } ) ;
1319 return < button { ...props } ref = { buttonRef } tabIndex = { tabIndex } /> ;
1420}
1521
16- test ( 'subscribed components can be rendered outside single tab stop navigation context' , ( ) => {
17- render ( < Button /> ) ;
18- expect ( document . querySelector ( 'button' ) ) . not . toHaveAttribute ( 'tabIndex' ) ;
19- } ) ;
22+ // Simple STSN provider component
23+ function Group ( {
24+ id,
25+ navigationActive,
26+ children,
27+ } : {
28+ id : string ;
29+ navigationActive : boolean ;
30+ children : React . ReactNode ;
31+ } ) {
32+ const navigationAPI = useRef < SingleTabStopNavigationAPI > ( null ) ;
33+
34+ useEffect ( ( ) => {
35+ navigationAPI . current ?. updateFocusTarget ( ) ;
36+ } ) ;
37+
38+ return (
39+ < SingleTabStopNavigationProvider
40+ ref = { navigationAPI }
41+ navigationActive = { navigationActive }
42+ getNextFocusTarget = { ( ) => document . querySelector ( `#${ id } ` ) ! . querySelectorAll ( 'button' ) [ 0 ] as HTMLElement }
43+ >
44+ < div id = { id } >
45+ < Button > First</ Button >
46+ < Button > Second</ Button >
47+ { children }
48+ </ div >
49+ </ SingleTabStopNavigationProvider >
50+ ) ;
51+ }
52+
53+ function findGroupButton ( groupId : string , buttonIndex : number ) {
54+ return document . querySelector ( `#${ groupId } ` ) ! . querySelectorAll ( 'button' ) [ buttonIndex ] as HTMLElement ;
55+ }
2056
2157test ( 'does not override tab index when keyboard navigation is not active' , ( ) => {
2258 renderWithSingleTabStopNavigation ( < Button id = "button" /> , { navigationActive : false } ) ;
@@ -34,14 +70,11 @@ test('does not override tab index for suppressed elements', () => {
3470 </ div > ,
3571 { navigationActive : true }
3672 ) ;
37- act ( ( ) => {
38- setCurrentTarget ( document . querySelector ( '#button1' ) , [
39- document . querySelector ( '#button1' ) ,
40- document . querySelector ( '#button2' ) ,
41- document . querySelector ( '#button3' ) ,
42- ] ) ;
43- } ) ;
44-
73+ setCurrentTarget ( document . querySelector ( '#button1' ) , [
74+ document . querySelector ( '#button1' ) ,
75+ document . querySelector ( '#button2' ) ,
76+ document . querySelector ( '#button3' ) ,
77+ ] ) ;
4578 expect ( document . querySelector ( '#button1' ) ) . toHaveAttribute ( 'tabIndex' , '0' ) ;
4679 expect ( document . querySelector ( '#button2' ) ) . toHaveAttribute ( 'tabIndex' , '0' ) ;
4780 expect ( document . querySelector ( '#button3' ) ) . toHaveAttribute ( 'tabIndex' , '-1' ) ;
@@ -56,9 +89,7 @@ test('overrides tab index when keyboard navigation is active', () => {
5689 < Button id = "button2" />
5790 </ div >
5891 ) ;
59- act ( ( ) => {
60- setCurrentTarget ( document . querySelector ( '#button1' ) ) ;
61- } ) ;
92+ setCurrentTarget ( document . querySelector ( '#button1' ) ) ;
6293 expect ( document . querySelector ( '#button1' ) ) . toHaveAttribute ( 'tabIndex' , '0' ) ;
6394 expect ( document . querySelector ( '#button2' ) ) . toHaveAttribute ( 'tabIndex' , '-1' ) ;
6495} ) ;
@@ -70,9 +101,7 @@ test('does not override explicit tab index with 0', () => {
70101 < Button id = "button2" tabIndex = { - 2 } />
71102 </ div >
72103 ) ;
73- act ( ( ) => {
74- setCurrentTarget ( document . querySelector ( '#button1' ) ) ;
75- } ) ;
104+ setCurrentTarget ( document . querySelector ( '#button1' ) ) ;
76105 expect ( document . querySelector ( '#button1' ) ) . toHaveAttribute ( 'tabIndex' , '-2' ) ;
77106 expect ( document . querySelector ( '#button2' ) ) . toHaveAttribute ( 'tabIndex' , '-2' ) ;
78107} ) ;
@@ -84,7 +113,9 @@ test('propagates and suppresses navigation active state', () => {
84113 }
85114 function Test ( { navigationActive } : { navigationActive : boolean } ) {
86115 return (
87- < SingleTabStopNavigationContext . Provider value = { { navigationActive, registerFocusable : ( ) => ( ) => { } } } >
116+ < SingleTabStopNavigationContext . Provider
117+ value = { { navigationActive, registerFocusable : ( ) => ( ) => { } , resetFocusTarget : ( ) => { } } }
118+ >
88119 < Component />
89120 </ SingleTabStopNavigationContext . Provider >
90121 ) ;
@@ -96,3 +127,124 @@ test('propagates and suppresses navigation active state', () => {
96127 rerender ( < Test navigationActive = { false } /> ) ;
97128 expect ( document . querySelector ( 'div' ) ) . toHaveTextContent ( 'false' ) ;
98129} ) ;
130+
131+ test ( 'subscriber components can be used without provider' , ( ) => {
132+ function TestComponent ( props : React . HTMLAttributes < HTMLButtonElement > ) {
133+ const ref = useRef ( null ) ;
134+ const contextResult = useContext ( SingleTabStopNavigationContext ) ;
135+ const hookResult = useSingleTabStopNavigation ( ref , { tabIndex : props . tabIndex } ) ;
136+ useEffect ( ( ) => {
137+ contextResult . registerFocusable ( ref . current ! , ( ) => { } ) ;
138+ contextResult . resetFocusTarget ( ) ;
139+ } ) ;
140+ return (
141+ < div ref = { ref } >
142+ Context: { `${ contextResult . navigationActive } ` } , Hook: { `${ hookResult . navigationActive } :${ hookResult . tabIndex } ` }
143+ </ div >
144+ ) ;
145+ }
146+ const { container } = render ( < TestComponent /> ) ;
147+ expect ( container . textContent ) . toBe ( 'Context: false, Hook: false:undefined' ) ;
148+ } ) ;
149+
150+ describe ( 'nested contexts' , ( ) => {
151+ test ( 'tab indices are distributed correctly when switching contexts from inner to outer' , ( ) => {
152+ const { rerender } = render (
153+ < Group id = "outer-most" navigationActive = { false } >
154+ < Group id = "outer" navigationActive = { false } >
155+ < Group id = "inner" navigationActive = { true } >
156+ { null }
157+ </ Group >
158+ </ Group >
159+ </ Group >
160+ ) ;
161+ expect ( findGroupButton ( 'outer-most' , 0 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
162+ expect ( findGroupButton ( 'outer-most' , 1 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
163+ expect ( findGroupButton ( 'outer' , 0 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
164+ expect ( findGroupButton ( 'outer' , 1 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
165+ expect ( findGroupButton ( 'inner' , 0 ) ) . toHaveAttribute ( 'tabindex' , '0' ) ;
166+ expect ( findGroupButton ( 'inner' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
167+
168+ rerender (
169+ < Group id = "outer-most" navigationActive = { false } >
170+ < Group id = "outer" navigationActive = { true } >
171+ < Group id = "inner" navigationActive = { true } >
172+ { null }
173+ </ Group >
174+ </ Group >
175+ </ Group >
176+ ) ;
177+ expect ( findGroupButton ( 'outer-most' , 0 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
178+ expect ( findGroupButton ( 'outer-most' , 1 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
179+ expect ( findGroupButton ( 'outer' , 0 ) ) . toHaveAttribute ( 'tabindex' , '0' ) ;
180+ expect ( findGroupButton ( 'outer' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
181+ expect ( findGroupButton ( 'inner' , 0 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
182+ expect ( findGroupButton ( 'inner' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
183+
184+ rerender (
185+ < Group id = "outer-most" navigationActive = { true } >
186+ < Group id = "outer" navigationActive = { true } >
187+ < Group id = "inner" navigationActive = { true } >
188+ { null }
189+ </ Group >
190+ </ Group >
191+ </ Group >
192+ ) ;
193+ expect ( findGroupButton ( 'outer-most' , 0 ) ) . toHaveAttribute ( 'tabindex' , '0' ) ;
194+ expect ( findGroupButton ( 'outer-most' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
195+ expect ( findGroupButton ( 'outer' , 0 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
196+ expect ( findGroupButton ( 'outer' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
197+ expect ( findGroupButton ( 'inner' , 0 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
198+ expect ( findGroupButton ( 'inner' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
199+ } ) ;
200+
201+ test ( 'tab indices are distributed correctly when switching contexts from outer to inner' , ( ) => {
202+ const { rerender } = render (
203+ < Group id = "outer-most" navigationActive = { true } >
204+ < Group id = "outer" navigationActive = { true } >
205+ < Group id = "inner" navigationActive = { true } >
206+ { null }
207+ </ Group >
208+ </ Group >
209+ </ Group >
210+ ) ;
211+ expect ( findGroupButton ( 'outer-most' , 0 ) ) . toHaveAttribute ( 'tabindex' , '0' ) ;
212+ expect ( findGroupButton ( 'outer-most' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
213+ expect ( findGroupButton ( 'outer' , 0 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
214+ expect ( findGroupButton ( 'outer' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
215+ expect ( findGroupButton ( 'inner' , 0 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
216+ expect ( findGroupButton ( 'inner' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
217+
218+ rerender (
219+ < Group id = "outer-most" navigationActive = { false } >
220+ < Group id = "outer" navigationActive = { true } >
221+ < Group id = "inner" navigationActive = { true } >
222+ { null }
223+ </ Group >
224+ </ Group >
225+ </ Group >
226+ ) ;
227+ expect ( findGroupButton ( 'outer-most' , 0 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
228+ expect ( findGroupButton ( 'outer-most' , 1 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
229+ expect ( findGroupButton ( 'outer' , 0 ) ) . toHaveAttribute ( 'tabindex' , '0' ) ;
230+ expect ( findGroupButton ( 'outer' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
231+ expect ( findGroupButton ( 'inner' , 0 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
232+ expect ( findGroupButton ( 'inner' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
233+
234+ rerender (
235+ < Group id = "outer-most" navigationActive = { false } >
236+ < Group id = "outer" navigationActive = { false } >
237+ < Group id = "inner" navigationActive = { true } >
238+ { null }
239+ </ Group >
240+ </ Group >
241+ </ Group >
242+ ) ;
243+ expect ( findGroupButton ( 'outer-most' , 0 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
244+ expect ( findGroupButton ( 'outer-most' , 1 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
245+ expect ( findGroupButton ( 'outer' , 0 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
246+ expect ( findGroupButton ( 'outer' , 1 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
247+ expect ( findGroupButton ( 'inner' , 0 ) ) . toHaveAttribute ( 'tabindex' , '0' ) ;
248+ expect ( findGroupButton ( 'inner' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
249+ } ) ;
250+ } ) ;
0 commit comments