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+ SingleTabStopNavigationReset ,
12+ useSingleTabStopNavigation ,
13+ } from '../' ;
814import { renderWithSingleTabStopNavigation } from './utils' ;
915
16+ // Simple STSN subscriber component
1017function Button ( props : React . HTMLAttributes < HTMLButtonElement > ) {
1118 const buttonRef = useRef < HTMLButtonElement > ( null ) ;
1219 const { tabIndex } = useSingleTabStopNavigation ( buttonRef , { tabIndex : props . tabIndex } ) ;
1320 return < button { ...props } ref = { buttonRef } tabIndex = { tabIndex } /> ;
1421}
1522
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- } ) ;
23+ // Simple STSN provider component
24+ function Group ( {
25+ id,
26+ navigationActive,
27+ children,
28+ } : {
29+ id : string ;
30+ navigationActive : boolean ;
31+ children : React . ReactNode ;
32+ } ) {
33+ const navigationAPI = useRef < SingleTabStopNavigationAPI > ( null ) ;
34+
35+ useEffect ( ( ) => {
36+ navigationAPI . current ?. updateFocusTarget ( ) ;
37+ } ) ;
38+
39+ return (
40+ < SingleTabStopNavigationProvider
41+ ref = { navigationAPI }
42+ navigationActive = { navigationActive }
43+ getNextFocusTarget = { ( ) => document . querySelector ( `#${ id } ` ) ! . querySelectorAll ( 'button' ) [ 0 ] as HTMLElement }
44+ >
45+ < div id = { id } >
46+ < Button > First</ Button >
47+ < Button > Second</ Button >
48+ { children }
49+ </ div >
50+ </ SingleTabStopNavigationProvider >
51+ ) ;
52+ }
53+
54+ function findGroupButton ( groupId : string , buttonIndex : number ) {
55+ return document . querySelector ( `#${ groupId } ` ) ! . querySelectorAll ( 'button' ) [ buttonIndex ] as HTMLElement ;
56+ }
2057
2158test ( 'does not override tab index when keyboard navigation is not active' , ( ) => {
2259 renderWithSingleTabStopNavigation ( < Button id = "button" /> , { navigationActive : false } ) ;
@@ -34,14 +71,11 @@ test('does not override tab index for suppressed elements', () => {
3471 </ div > ,
3572 { navigationActive : true }
3673 ) ;
37- act ( ( ) => {
38- setCurrentTarget ( document . querySelector ( '#button1' ) , [
39- document . querySelector ( '#button1' ) ,
40- document . querySelector ( '#button2' ) ,
41- document . querySelector ( '#button3' ) ,
42- ] ) ;
43- } ) ;
44-
74+ setCurrentTarget ( document . querySelector ( '#button1' ) , [
75+ document . querySelector ( '#button1' ) ,
76+ document . querySelector ( '#button2' ) ,
77+ document . querySelector ( '#button3' ) ,
78+ ] ) ;
4579 expect ( document . querySelector ( '#button1' ) ) . toHaveAttribute ( 'tabIndex' , '0' ) ;
4680 expect ( document . querySelector ( '#button2' ) ) . toHaveAttribute ( 'tabIndex' , '0' ) ;
4781 expect ( document . querySelector ( '#button3' ) ) . toHaveAttribute ( 'tabIndex' , '-1' ) ;
@@ -56,9 +90,7 @@ test('overrides tab index when keyboard navigation is active', () => {
5690 < Button id = "button2" />
5791 </ div >
5892 ) ;
59- act ( ( ) => {
60- setCurrentTarget ( document . querySelector ( '#button1' ) ) ;
61- } ) ;
93+ setCurrentTarget ( document . querySelector ( '#button1' ) ) ;
6294 expect ( document . querySelector ( '#button1' ) ) . toHaveAttribute ( 'tabIndex' , '0' ) ;
6395 expect ( document . querySelector ( '#button2' ) ) . toHaveAttribute ( 'tabIndex' , '-1' ) ;
6496} ) ;
@@ -70,9 +102,7 @@ test('does not override explicit tab index with 0', () => {
70102 < Button id = "button2" tabIndex = { - 2 } />
71103 </ div >
72104 ) ;
73- act ( ( ) => {
74- setCurrentTarget ( document . querySelector ( '#button1' ) ) ;
75- } ) ;
105+ setCurrentTarget ( document . querySelector ( '#button1' ) ) ;
76106 expect ( document . querySelector ( '#button1' ) ) . toHaveAttribute ( 'tabIndex' , '-2' ) ;
77107 expect ( document . querySelector ( '#button2' ) ) . toHaveAttribute ( 'tabIndex' , '-2' ) ;
78108} ) ;
@@ -84,7 +114,9 @@ test('propagates and suppresses navigation active state', () => {
84114 }
85115 function Test ( { navigationActive } : { navigationActive : boolean } ) {
86116 return (
87- < SingleTabStopNavigationContext . Provider value = { { navigationActive, registerFocusable : ( ) => ( ) => { } } } >
117+ < SingleTabStopNavigationContext . Provider
118+ value = { { navigationActive, registerFocusable : ( ) => ( ) => { } , resetFocusTarget : ( ) => { } } }
119+ >
88120 < Component />
89121 </ SingleTabStopNavigationContext . Provider >
90122 ) ;
@@ -96,3 +128,162 @@ test('propagates and suppresses navigation active state', () => {
96128 rerender ( < Test navigationActive = { false } /> ) ;
97129 expect ( document . querySelector ( 'div' ) ) . toHaveTextContent ( 'false' ) ;
98130} ) ;
131+
132+ test ( 'subscriber components can be used without provider' , ( ) => {
133+ function TestComponent ( props : React . HTMLAttributes < HTMLButtonElement > ) {
134+ const ref = useRef ( null ) ;
135+ const contextResult = useContext ( SingleTabStopNavigationContext ) ;
136+ const hookResult = useSingleTabStopNavigation ( ref , { tabIndex : props . tabIndex } ) ;
137+ useEffect ( ( ) => {
138+ contextResult . registerFocusable ( ref . current ! , ( ) => { } ) ;
139+ contextResult . resetFocusTarget ( ) ;
140+ } ) ;
141+ return (
142+ < div ref = { ref } >
143+ Context: { `${ contextResult . navigationActive } ` } , Hook: { `${ hookResult . navigationActive } :${ hookResult . tabIndex } ` }
144+ </ div >
145+ ) ;
146+ }
147+ const { container } = render ( < TestComponent /> ) ;
148+ expect ( container . textContent ) . toBe ( 'Context: false, Hook: false:undefined' ) ;
149+ } ) ;
150+
151+ describe ( 'nested contexts' , ( ) => {
152+ test ( 'tab indices are distributed correctly when switching contexts from inner to outer' , ( ) => {
153+ const { rerender } = render (
154+ < Group id = "outer-most" navigationActive = { false } >
155+ < Group id = "outer" navigationActive = { false } >
156+ < Group id = "inner" navigationActive = { true } >
157+ { null }
158+ </ Group >
159+ </ Group >
160+ </ Group >
161+ ) ;
162+ expect ( findGroupButton ( 'outer-most' , 0 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
163+ expect ( findGroupButton ( 'outer-most' , 1 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
164+ expect ( findGroupButton ( 'outer' , 0 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
165+ expect ( findGroupButton ( 'outer' , 1 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
166+ expect ( findGroupButton ( 'inner' , 0 ) ) . toHaveAttribute ( 'tabindex' , '0' ) ;
167+ expect ( findGroupButton ( 'inner' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
168+
169+ rerender (
170+ < Group id = "outer-most" navigationActive = { false } >
171+ < Group id = "outer" navigationActive = { true } >
172+ < Group id = "inner" navigationActive = { true } >
173+ { null }
174+ </ Group >
175+ </ Group >
176+ </ Group >
177+ ) ;
178+ expect ( findGroupButton ( 'outer-most' , 0 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
179+ expect ( findGroupButton ( 'outer-most' , 1 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
180+ expect ( findGroupButton ( 'outer' , 0 ) ) . toHaveAttribute ( 'tabindex' , '0' ) ;
181+ expect ( findGroupButton ( 'outer' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
182+ expect ( findGroupButton ( 'inner' , 0 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
183+ expect ( findGroupButton ( 'inner' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
184+
185+ rerender (
186+ < Group id = "outer-most" navigationActive = { true } >
187+ < Group id = "outer" navigationActive = { true } >
188+ < Group id = "inner" navigationActive = { true } >
189+ { null }
190+ </ Group >
191+ </ Group >
192+ </ Group >
193+ ) ;
194+ expect ( findGroupButton ( 'outer-most' , 0 ) ) . toHaveAttribute ( 'tabindex' , '0' ) ;
195+ expect ( findGroupButton ( 'outer-most' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
196+ expect ( findGroupButton ( 'outer' , 0 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
197+ expect ( findGroupButton ( 'outer' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
198+ expect ( findGroupButton ( 'inner' , 0 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
199+ expect ( findGroupButton ( 'inner' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
200+ } ) ;
201+
202+ test ( 'tab indices are distributed correctly when switching contexts from outer to inner' , ( ) => {
203+ const { rerender } = render (
204+ < Group id = "outer-most" navigationActive = { true } >
205+ < Group id = "outer" navigationActive = { true } >
206+ < Group id = "inner" navigationActive = { true } >
207+ { null }
208+ </ Group >
209+ </ Group >
210+ </ Group >
211+ ) ;
212+ expect ( findGroupButton ( 'outer-most' , 0 ) ) . toHaveAttribute ( 'tabindex' , '0' ) ;
213+ expect ( findGroupButton ( 'outer-most' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
214+ expect ( findGroupButton ( 'outer' , 0 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
215+ expect ( findGroupButton ( 'outer' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
216+ expect ( findGroupButton ( 'inner' , 0 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
217+ expect ( findGroupButton ( 'inner' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
218+
219+ rerender (
220+ < Group id = "outer-most" navigationActive = { false } >
221+ < Group id = "outer" navigationActive = { true } >
222+ < Group id = "inner" navigationActive = { true } >
223+ { null }
224+ </ Group >
225+ </ Group >
226+ </ Group >
227+ ) ;
228+ expect ( findGroupButton ( 'outer-most' , 0 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
229+ expect ( findGroupButton ( 'outer-most' , 1 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
230+ expect ( findGroupButton ( 'outer' , 0 ) ) . toHaveAttribute ( 'tabindex' , '0' ) ;
231+ expect ( findGroupButton ( 'outer' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
232+ expect ( findGroupButton ( 'inner' , 0 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
233+ expect ( findGroupButton ( 'inner' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
234+
235+ rerender (
236+ < Group id = "outer-most" navigationActive = { false } >
237+ < Group id = "outer" navigationActive = { false } >
238+ < Group id = "inner" navigationActive = { true } >
239+ { null }
240+ </ Group >
241+ </ Group >
242+ </ Group >
243+ ) ;
244+ expect ( findGroupButton ( 'outer-most' , 0 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
245+ expect ( findGroupButton ( 'outer-most' , 1 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
246+ expect ( findGroupButton ( 'outer' , 0 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
247+ expect ( findGroupButton ( 'outer' , 1 ) ) . not . toHaveAttribute ( 'tabindex' ) ;
248+ expect ( findGroupButton ( 'inner' , 0 ) ) . toHaveAttribute ( 'tabindex' , '0' ) ;
249+ expect ( findGroupButton ( 'inner' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
250+ } ) ;
251+
252+ test ( 'ignores parent context when reset is used' , ( ) => {
253+ const { rerender } = render (
254+ < Group id = "outer-most" navigationActive = { true } >
255+ < SingleTabStopNavigationReset >
256+ < Group id = "outer" navigationActive = { true } >
257+ < Group id = "inner" navigationActive = { true } >
258+ { null }
259+ </ Group >
260+ </ Group >
261+ </ SingleTabStopNavigationReset >
262+ </ Group >
263+ ) ;
264+ expect ( findGroupButton ( 'outer-most' , 0 ) ) . toHaveAttribute ( 'tabindex' , '0' ) ;
265+ expect ( findGroupButton ( 'outer-most' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
266+ expect ( findGroupButton ( 'outer' , 0 ) ) . toHaveAttribute ( 'tabindex' , '0' ) ;
267+ expect ( findGroupButton ( 'outer' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
268+ expect ( findGroupButton ( 'inner' , 0 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
269+ expect ( findGroupButton ( 'inner' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
270+
271+ rerender (
272+ < Group id = "outer-most" navigationActive = { true } >
273+ < Group id = "outer" navigationActive = { true } >
274+ < SingleTabStopNavigationReset >
275+ < Group id = "inner" navigationActive = { true } >
276+ { null }
277+ </ Group >
278+ </ SingleTabStopNavigationReset >
279+ </ Group >
280+ </ Group >
281+ ) ;
282+ expect ( findGroupButton ( 'outer-most' , 0 ) ) . toHaveAttribute ( 'tabindex' , '0' ) ;
283+ expect ( findGroupButton ( 'outer-most' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
284+ expect ( findGroupButton ( 'outer' , 0 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
285+ expect ( findGroupButton ( 'outer' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
286+ expect ( findGroupButton ( 'inner' , 0 ) ) . toHaveAttribute ( 'tabindex' , '0' ) ;
287+ expect ( findGroupButton ( 'inner' , 1 ) ) . toHaveAttribute ( 'tabindex' , '-1' ) ;
288+ } ) ;
289+ } ) ;
0 commit comments