@@ -7,7 +7,7 @@ import { TabTitleText } from '../TabTitleText';
77import { TabTitleIcon } from '../TabTitleIcon' ;
88import { TabContent } from '../TabContent' ;
99import { TabContentBody } from '../TabContentBody' ;
10- import { createRef } from 'react' ;
10+ import { createRef , useState } from 'react' ;
1111
1212jest . mock ( '../../../helpers/GenerateId/GenerateId' ) ;
1313
@@ -119,6 +119,222 @@ test(`Does not render with class ${styles.modifiers.initializingAccent} when com
119119 jest . useRealTimers ( ) ;
120120} ) ;
121121
122+ describe ( 'hash-based nav selection' , ( ) => {
123+ beforeEach ( ( ) => {
124+ window . location . hash = '#/items/2' ;
125+ } ) ;
126+
127+ afterEach ( ( ) => {
128+ window . location . hash = '' ;
129+ } ) ;
130+
131+ test ( 'should select the nav tab that matches the current URL hash on initial render' , ( ) => {
132+ render (
133+ < Tabs activeKey = { 0 } onSelect = { ( ) => undefined } component = "nav" >
134+ < Tab eventKey = { 0 } title = { < TabTitleText > Tab item 1</ TabTitleText > } href = "#/items/1" >
135+ Tab item 1
136+ </ Tab >
137+ < Tab eventKey = { 1 } title = { < TabTitleText > Tab item 2</ TabTitleText > } href = "#/items/2" >
138+ Tab item 2
139+ </ Tab >
140+ < Tab eventKey = { 2 } title = { < TabTitleText > Tab item 3</ TabTitleText > } href = "#/items/3" >
141+ Tab item 3
142+ </ Tab >
143+ </ Tabs >
144+ ) ;
145+
146+ expect ( screen . getByRole ( 'tab' , { name : 'Tab item 2' } ) ) . toHaveAttribute ( 'aria-selected' , 'true' ) ;
147+ expect ( screen . getByRole ( 'tab' , { name : 'Tab item 1' } ) ) . toHaveAttribute ( 'aria-selected' , 'false' ) ;
148+ } ) ;
149+
150+ test ( 'should select the nav tab that matches the current URL hash when isNav is enabled' , ( ) => {
151+ render (
152+ < Tabs activeKey = { 0 } onSelect = { ( ) => undefined } isNav >
153+ < Tab eventKey = { 0 } title = { < TabTitleText > Tab item 1</ TabTitleText > } href = "#/items/1" >
154+ Tab item 1
155+ </ Tab >
156+ < Tab eventKey = { 1 } title = { < TabTitleText > Tab item 2</ TabTitleText > } href = "#/items/2" >
157+ Tab item 2
158+ </ Tab >
159+ </ Tabs >
160+ ) ;
161+
162+ expect ( screen . getByRole ( 'tab' , { name : 'Tab item 2' } ) ) . toHaveAttribute ( 'aria-selected' , 'true' ) ;
163+ expect ( screen . getByRole ( 'tab' , { name : 'Tab item 1' } ) ) . toHaveAttribute ( 'aria-selected' , 'false' ) ;
164+ } ) ;
165+
166+ test ( 'should fall back to activeKey when no tab href matches the current hash' , ( ) => {
167+ window . location . hash = '#/items/unknown' ;
168+
169+ render (
170+ < Tabs activeKey = { 0 } onSelect = { ( ) => undefined } component = "nav" >
171+ < Tab eventKey = { 0 } title = { < TabTitleText > Tab item 1</ TabTitleText > } href = "#/items/1" >
172+ Tab item 1
173+ </ Tab >
174+ < Tab eventKey = { 1 } title = { < TabTitleText > Tab item 2</ TabTitleText > } href = "#/items/2" >
175+ Tab item 2
176+ </ Tab >
177+ </ Tabs >
178+ ) ;
179+
180+ expect ( screen . getByRole ( 'tab' , { name : 'Tab item 1' } ) ) . toHaveAttribute ( 'aria-selected' , 'true' ) ;
181+ expect ( screen . getByRole ( 'tab' , { name : 'Tab item 2' } ) ) . toHaveAttribute ( 'aria-selected' , 'false' ) ;
182+ } ) ;
183+
184+ test ( 'should fall back to activeKey when the URL hash is empty' , ( ) => {
185+ window . location . hash = '' ;
186+
187+ render (
188+ < Tabs activeKey = { 0 } onSelect = { ( ) => undefined } component = "nav" >
189+ < Tab eventKey = { 0 } title = { < TabTitleText > Tab item 1</ TabTitleText > } href = "#/items/1" >
190+ Tab item 1
191+ </ Tab >
192+ < Tab eventKey = { 1 } title = { < TabTitleText > Tab item 2</ TabTitleText > } href = "#/items/2" >
193+ Tab item 2
194+ </ Tab >
195+ </ Tabs >
196+ ) ;
197+
198+ expect ( screen . getByRole ( 'tab' , { name : 'Tab item 1' } ) ) . toHaveAttribute ( 'aria-selected' , 'true' ) ;
199+ expect ( screen . getByRole ( 'tab' , { name : 'Tab item 2' } ) ) . toHaveAttribute ( 'aria-selected' , 'false' ) ;
200+ } ) ;
201+
202+ test ( 'should respect later controlled selections after the initial hash match' , async ( ) => {
203+ const user = userEvent . setup ( ) ;
204+ const ControlledTabs = ( ) => {
205+ const [ activeKey , setActiveKey ] = useState < string | number > ( 0 ) ;
206+
207+ return (
208+ < Tabs activeKey = { activeKey } onSelect = { ( _event , eventKey ) => setActiveKey ( eventKey ) } component = "nav" >
209+ < Tab eventKey = { 0 } title = { < TabTitleText > Tab item 1</ TabTitleText > } href = "#/items/1" >
210+ Tab item 1
211+ </ Tab >
212+ < Tab eventKey = { 1 } title = { < TabTitleText > Tab item 2</ TabTitleText > } href = "#/items/2" >
213+ Tab item 2
214+ </ Tab >
215+ < Tab eventKey = { 2 } title = { < TabTitleText > Tab item 3</ TabTitleText > } href = "#/items/3" >
216+ Tab item 3
217+ </ Tab >
218+ </ Tabs >
219+ ) ;
220+ } ;
221+
222+ render ( < ControlledTabs /> ) ;
223+
224+ expect ( screen . getByRole ( 'tab' , { name : 'Tab item 2' } ) ) . toHaveAttribute ( 'aria-selected' , 'true' ) ;
225+
226+ await user . click ( screen . getByRole ( 'tab' , { name : 'Tab item 3' } ) ) ;
227+
228+ expect ( screen . getByRole ( 'tab' , { name : 'Tab item 3' } ) ) . toHaveAttribute ( 'aria-selected' , 'true' ) ;
229+ expect ( screen . getByRole ( 'tab' , { name : 'Tab item 2' } ) ) . toHaveAttribute ( 'aria-selected' , 'false' ) ;
230+ } ) ;
231+
232+ test ( 'should use the URL hash to initialize uncontrolled nav tabs' , async ( ) => {
233+ const user = userEvent . setup ( ) ;
234+
235+ render (
236+ < Tabs defaultActiveKey = { 0 } isNav >
237+ < Tab eventKey = { 0 } title = { < TabTitleText > Tab item 1</ TabTitleText > } href = "#/items/1" >
238+ Tab item 1
239+ </ Tab >
240+ < Tab eventKey = { 1 } title = { < TabTitleText > Tab item 2</ TabTitleText > } href = "#/items/2" >
241+ Tab item 2
242+ </ Tab >
243+ < Tab eventKey = { 2 } title = { < TabTitleText > Tab item 3</ TabTitleText > } href = "#/items/3" >
244+ Tab item 3
245+ </ Tab >
246+ </ Tabs >
247+ ) ;
248+
249+ expect ( screen . getByRole ( 'tab' , { name : 'Tab item 2' } ) ) . toHaveAttribute ( 'aria-selected' , 'true' ) ;
250+
251+ await user . click ( screen . getByRole ( 'tab' , { name : 'Tab item 1' } ) ) ;
252+
253+ expect ( screen . getByRole ( 'tab' , { name : 'Tab item 1' } ) ) . toHaveAttribute ( 'aria-selected' , 'true' ) ;
254+ expect ( screen . getByRole ( 'tab' , { name : 'Tab item 2' } ) ) . toHaveAttribute ( 'aria-selected' , 'false' ) ;
255+ } ) ;
256+
257+ test ( 'should ignore hidden, disabled and aria-disabled tabs when matching the current hash' , ( ) => {
258+ render (
259+ < Tabs activeKey = { 0 } onSelect = { ( ) => undefined } component = "nav" >
260+ < Tab eventKey = { 0 } title = { < TabTitleText > Tab item 1</ TabTitleText > } href = "#/items/1" >
261+ Tab item 1
262+ </ Tab >
263+ < Tab eventKey = { 1 } title = { < TabTitleText > Hidden tab</ TabTitleText > } href = "#/items/2" isHidden >
264+ Hidden tab
265+ </ Tab >
266+ < Tab eventKey = { 2 } title = { < TabTitleText > Disabled tab</ TabTitleText > } href = "#/items/2" isDisabled >
267+ Disabled tab
268+ </ Tab >
269+ < Tab eventKey = { 3 } title = { < TabTitleText > Aria disabled tab</ TabTitleText > } href = "#/items/2" isAriaDisabled >
270+ Aria disabled tab
271+ </ Tab >
272+ </ Tabs >
273+ ) ;
274+
275+ expect ( screen . queryByRole ( 'tab' , { name : 'Hidden tab' } ) ) . not . toBeInTheDocument ( ) ;
276+ expect ( screen . getByRole ( 'tab' , { name : 'Tab item 1' } ) ) . toHaveAttribute ( 'aria-selected' , 'true' ) ;
277+ expect ( screen . getByRole ( 'tab' , { name : 'Disabled tab' } ) ) . toHaveAttribute ( 'aria-selected' , 'false' ) ;
278+ expect ( screen . getByRole ( 'tab' , { name : 'Aria disabled tab' } ) ) . toHaveAttribute ( 'aria-selected' , 'false' ) ;
279+ } ) ;
280+
281+ test ( 'should ignore the current URL hash when nav behavior is not enabled' , ( ) => {
282+ render (
283+ < Tabs activeKey = { 0 } onSelect = { ( ) => undefined } >
284+ < Tab eventKey = { 0 } title = { < TabTitleText > Tab item 1</ TabTitleText > } href = "#/items/1" >
285+ Tab item 1
286+ </ Tab >
287+ < Tab eventKey = { 1 } title = { < TabTitleText > Tab item 2</ TabTitleText > } href = "#/items/2" >
288+ Tab item 2
289+ </ Tab >
290+ </ Tabs >
291+ ) ;
292+
293+ expect ( screen . getByRole ( 'tab' , { name : 'Tab item 1' } ) ) . toHaveAttribute ( 'aria-selected' , 'true' ) ;
294+ expect ( screen . getByRole ( 'tab' , { name : 'Tab item 2' } ) ) . toHaveAttribute ( 'aria-selected' , 'false' ) ;
295+ } ) ;
296+
297+ test ( 'should prevent default anchor navigation when selecting a tab with an href' , async ( ) => {
298+ const user = userEvent . setup ( ) ;
299+ window . location . hash = '' ;
300+
301+ render (
302+ < Tabs activeKey = { 0 } onSelect = { ( ) => undefined } >
303+ < Tab eventKey = { 0 } title = { < TabTitleText > Tab item 1</ TabTitleText > } href = "#/items/1" >
304+ Tab item 1
305+ </ Tab >
306+ < Tab eventKey = { 1 } title = { < TabTitleText > Tab item 2</ TabTitleText > } href = "#/items/2" >
307+ Tab item 2
308+ </ Tab >
309+ </ Tabs >
310+ ) ;
311+
312+ await user . click ( screen . getByRole ( 'tab' , { name : 'Tab item 2' } ) ) ;
313+
314+ expect ( window . location . hash ) . toBe ( '' ) ;
315+ } ) ;
316+
317+ test ( 'should update the URL hash when selecting a nav tab with an href' , async ( ) => {
318+ const user = userEvent . setup ( ) ;
319+ window . location . hash = '' ;
320+
321+ render (
322+ < Tabs activeKey = { 0 } onSelect = { ( ) => undefined } isNav >
323+ < Tab eventKey = { 0 } title = { < TabTitleText > Users</ TabTitleText > } href = "#users" >
324+ Users
325+ </ Tab >
326+ < Tab eventKey = { 1 } title = { < TabTitleText > Containers</ TabTitleText > } href = "#containers" >
327+ Containers
328+ </ Tab >
329+ </ Tabs >
330+ ) ;
331+
332+ await user . click ( screen . getByRole ( 'tab' , { name : 'Containers' } ) ) ;
333+
334+ expect ( window . location . hash ) . toBe ( '#containers' ) ;
335+ } ) ;
336+ } ) ;
337+
122338test ( `Renders with class ${ styles . modifiers . initializingAccent } when uncontrolled expandable component initially mounts` , async ( ) => {
123339 const user = userEvent . setup ( { advanceTimers : jest . advanceTimersByTime } ) ;
124340
0 commit comments