Skip to content

Commit 96be06e

Browse files
Dynamic tabs: use buttons rather than links (#32630)
* Dynamic tabs: use buttons rather than links - change docs - add mention that tabs should be <button> elements - tweak styles to neutralise border and background * Update js unit and visual test accordingly - replace links with buttons - make one specific test that uses links instead of buttons, as we still want to support it despite it being non-semantically appropriate - Leaving a couple of tests for now. The test for removed tabs should be redone so that tabs are removed programmatically (as the approach of having that close button inside the link is invalid and broken markup). The test for dropdowns should be removed together we actually ripping out the handling for dropdowns in the tab.js code (arguably a breaking change, though we discouraged this for a few versions and effectively "deprecated" it) * Add isolation:isolate to prevent focus being overlapped #32630 (comment)
1 parent c93d754 commit 96be06e

File tree

4 files changed

+148
-122
lines changed

4 files changed

+148
-122
lines changed

js/tests/unit/tab.spec.js

Lines changed: 74 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,39 @@ describe('Tab', () => {
2121
})
2222

2323
describe('show', () => {
24-
it('should activate element by tab id', done => {
24+
it('should activate element by tab id (using buttons, the preferred semantic way)', done => {
2525
fixtureEl.innerHTML = [
26-
'<ul class="nav">',
26+
'<ul class="nav" role="tablist">',
27+
' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>',
28+
' <li><button type="button" id="triggerProfile" data-bs-target="#profile" role="tab">Profile</button></li>',
29+
'</ul>',
30+
'<ul>',
31+
' <li id="home" role="tabpanel"></li>',
32+
' <li id="profile" role="tabpanel"></li>',
33+
'</ul>'
34+
].join('')
35+
36+
const profileTriggerEl = fixtureEl.querySelector('#triggerProfile')
37+
const tab = new Tab(profileTriggerEl)
38+
39+
profileTriggerEl.addEventListener('shown.bs.tab', () => {
40+
expect(fixtureEl.querySelector('#profile').classList.contains('active')).toEqual(true)
41+
expect(profileTriggerEl.getAttribute('aria-selected')).toEqual('true')
42+
done()
43+
})
44+
45+
tab.show()
46+
})
47+
48+
it('should activate element by tab id (using links for tabs - not ideal, but still supported)', done => {
49+
fixtureEl.innerHTML = [
50+
'<ul class="nav" role="tablist">',
2751
' <li><a href="#home" role="tab">Home</a></li>',
28-
' <li><a id="triggerProfile" role="tab" href="#profile">Profile</a></li>',
52+
' <li><a id="triggerProfile" href="#profile" role="tab">Profile</a></li>',
2953
'</ul>',
3054
'<ul>',
31-
' <li id="home"></li>',
32-
' <li id="profile"></li>',
55+
' <li id="home" role="tabpanel"></li>',
56+
' <li id="profile" role="tabpanel"></li>',
3357
'</ul>'
3458
].join('')
3559

@@ -48,12 +72,12 @@ describe('Tab', () => {
4872
it('should activate element by tab id in ordered list', done => {
4973
fixtureEl.innerHTML = [
5074
'<ol class="nav nav-pills">',
51-
' <li><a href="#home">Home</a></li>',
52-
' <li><a id="triggerProfile" href="#profile">Profile</a></li>',
75+
' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>',
76+
' <li><button type="button" id="triggerProfile" href="#profile" role="tab">Profile</button></li>',
5377
'</ol>',
5478
'<ol>',
55-
' <li id="home"></li>',
56-
' <li id="profile"></li>',
79+
' <li id="home" role="tabpanel"></li>',
80+
' <li id="profile" role="tabpanel"></li>',
5781
'</ol>'
5882
].join('')
5983

@@ -71,10 +95,10 @@ describe('Tab', () => {
7195
it('should activate element by tab id in nav list', done => {
7296
fixtureEl.innerHTML = [
7397
'<nav class="nav">',
74-
' <a href="#home">Home</a>',
75-
' <a id="triggerProfile" href="#profile">Profile</a>',
98+
' <button type="button" data-bs-target="#home" role="tab">Home</button>',
99+
' <button type="button" id="triggerProfile" data-bs-target="#profile" role="tab">Profile</a>',
76100
'</nav>',
77-
'<nav><div id="home"></div><div id="profile"></div></nav>'
101+
'<div><div id="home" role="tabpanel"></div><div id="profile" role="tabpanel"></div></div>'
78102
].join('')
79103

80104
const profileTriggerEl = fixtureEl.querySelector('#triggerProfile')
@@ -90,11 +114,11 @@ describe('Tab', () => {
90114

91115
it('should activate element by tab id in list group', done => {
92116
fixtureEl.innerHTML = [
93-
'<div class="list-group">',
94-
' <a href="#home">Home</a>',
95-
' <a id="triggerProfile" href="#profile">Profile</a>',
117+
'<div class="list-group" role="tablist">',
118+
' <button type="button" data-bs-target="#home" role="tab">Home</button>',
119+
' <button type="button" id="triggerProfile" data-bs-target="#profile" role="tab">Profile</button>',
96120
'</div>',
97-
'<nav><div id="home"></div><div id="profile"></div></nav>'
121+
'<div><div id="home" role="tabpanel"></div><div id="profile" role="tabpanel"></div></div>'
98122
].join('')
99123

100124
const profileTriggerEl = fixtureEl.querySelector('#triggerProfile')
@@ -135,16 +159,16 @@ describe('Tab', () => {
135159
it('should not fire shown when tab is already active', done => {
136160
fixtureEl.innerHTML = [
137161
'<ul class="nav nav-tabs" role="tablist">',
138-
' <li class="nav-item" role="presentation"><a href="#home" class="nav-link active" role="tab">Home</a></li>',
139-
' <li class="nav-item" role="presentation"><a href="#profile" class="nav-link" role="tab">Profile</a></li>',
162+
' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>',
163+
' <li class="nav-item" role="presentation"><button type="button" href="#profile" class="nav-link" role="tab">Profile</button></li>',
140164
'</ul>',
141165
'<div class="tab-content">',
142166
' <div class="tab-pane active" id="home" role="tabpanel"></div>',
143167
' <div class="tab-pane" id="profile" role="tabpanel"></div>',
144168
'</div>'
145169
].join('')
146170

147-
const triggerActive = fixtureEl.querySelector('a.active')
171+
const triggerActive = fixtureEl.querySelector('button.active')
148172
const tab = new Tab(triggerActive)
149173

150174
triggerActive.addEventListener('shown.bs.tab', () => {
@@ -161,16 +185,16 @@ describe('Tab', () => {
161185
it('should not fire shown when tab is disabled', done => {
162186
fixtureEl.innerHTML = [
163187
'<ul class="nav nav-tabs" role="tablist">',
164-
' <li class="nav-item" role="presentation"><a href="#home" class="nav-link active" role="tab">Home</a></li>',
165-
' <li class="nav-item" role="presentation"><a href="#profile" class="nav-link disabled" role="tab">Profile</a></li>',
188+
' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>',
189+
' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#profile" class="nav-link disabled" role="tab">Profile</button></li>',
166190
'</ul>',
167191
'<div class="tab-content">',
168192
' <div class="tab-pane active" id="home" role="tabpanel"></div>',
169193
' <div class="tab-pane" id="profile" role="tabpanel"></div>',
170194
'</div>'
171195
].join('')
172196

173-
const triggerDisabled = fixtureEl.querySelector('a.disabled')
197+
const triggerDisabled = fixtureEl.querySelector('button.disabled')
174198
const tab = new Tab(triggerDisabled)
175199

176200
triggerDisabled.addEventListener('shown.bs.tab', () => {
@@ -187,8 +211,8 @@ describe('Tab', () => {
187211
it('show and shown events should reference correct relatedTarget', done => {
188212
fixtureEl.innerHTML = [
189213
'<ul class="nav nav-tabs" role="tablist">',
190-
' <li class="nav-item" role="presentation"><a href="#home" class="nav-link active" role="tab">Home</a></li>',
191-
' <li class="nav-item" role="presentation"><a id="triggerProfile" href="#profile" class="nav-link" role="tab">Profile</a></li>',
214+
' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>',
215+
' <li class="nav-item" role="presentation"><button type="button" id="triggerProfile" data-bs-target="#profile" class="nav-link" role="tab">Profile</button></li>',
192216
'</ul>',
193217
'<div class="tab-content">',
194218
' <div class="tab-pane active" id="home" role="tabpanel"></div>',
@@ -200,13 +224,13 @@ describe('Tab', () => {
200224
const secondTab = new Tab(secondTabTrigger)
201225

202226
secondTabTrigger.addEventListener('show.bs.tab', ev => {
203-
expect(ev.relatedTarget.hash).toEqual('#home')
227+
expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#home')
204228
})
205229

206230
secondTabTrigger.addEventListener('shown.bs.tab', ev => {
207-
expect(ev.relatedTarget.hash).toEqual('#home')
231+
expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#home')
208232
expect(secondTabTrigger.getAttribute('aria-selected')).toEqual('true')
209-
expect(fixtureEl.querySelector('a:not(.active)').getAttribute('aria-selected')).toEqual('false')
233+
expect(fixtureEl.querySelector('button:not(.active)').getAttribute('aria-selected')).toEqual('false')
210234
done()
211235
})
212236

@@ -215,13 +239,13 @@ describe('Tab', () => {
215239

216240
it('should fire hide and hidden events', done => {
217241
fixtureEl.innerHTML = [
218-
'<ul class="nav">',
219-
' <li><a href="#home">Home</a></li>',
220-
' <li><a href="#profile">Profile</a></li>',
242+
'<ul class="nav" role="tablist">',
243+
' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>',
244+
' <li><button type="button" data-bs-target="#profile">Profile</button></li>',
221245
'</ul>'
222246
].join('')
223247

224-
const triggerList = fixtureEl.querySelectorAll('a')
248+
const triggerList = fixtureEl.querySelectorAll('button')
225249
const firstTab = new Tab(triggerList[0])
226250
const secondTab = new Tab(triggerList[1])
227251

@@ -232,12 +256,12 @@ describe('Tab', () => {
232256

233257
triggerList[0].addEventListener('hide.bs.tab', ev => {
234258
hideCalled = true
235-
expect(ev.relatedTarget.hash).toEqual('#profile')
259+
expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#profile')
236260
})
237261

238262
triggerList[0].addEventListener('hidden.bs.tab', ev => {
239263
expect(hideCalled).toEqual(true)
240-
expect(ev.relatedTarget.hash).toEqual('#profile')
264+
expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#profile')
241265
done()
242266
})
243267

@@ -246,13 +270,13 @@ describe('Tab', () => {
246270

247271
it('should not fire hidden when hide is prevented', done => {
248272
fixtureEl.innerHTML = [
249-
'<ul class="nav">',
250-
' <li><a href="#home">Home</a></li>',
251-
' <li><a href="#profile">Profile</a></li>',
273+
'<ul class="nav" role="tablist">',
274+
' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>',
275+
' <li><button type="button" data-bs-target="#profile" role="tab">Profile</button></li>',
252276
'</ul>'
253277
].join('')
254278

255-
const triggerList = fixtureEl.querySelectorAll('a')
279+
const triggerList = fixtureEl.querySelectorAll('button')
256280
const firstTab = new Tab(triggerList[0])
257281
const secondTab = new Tab(triggerList[1])
258282
const expectDone = () => {
@@ -423,8 +447,8 @@ describe('Tab', () => {
423447
it('should create dynamically a tab', done => {
424448
fixtureEl.innerHTML = [
425449
'<ul class="nav nav-tabs" role="tablist">',
426-
' <li class="nav-item" role="presentation"><a href="#home" class="nav-link active" role="tab">Home</a></li>',
427-
' <li class="nav-item" role="presentation"><a id="triggerProfile" data-bs-toggle="tab" href="#profile" class="nav-link" role="tab">Profile</a></li>',
450+
' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>',
451+
' <li class="nav-item" role="presentation"><button type="button" id="triggerProfile" data-bs-toggle="tab" data-bs-target="#profile" class="nav-link" role="tab">Profile</button></li>',
428452
'</ul>',
429453
'<div class="tab-content">',
430454
' <div class="tab-pane active" id="home" role="tabpanel"></div>',
@@ -469,15 +493,15 @@ describe('Tab', () => {
469493
it('should handle nested tabs', done => {
470494
fixtureEl.innerHTML = [
471495
'<nav class="nav nav-tabs" role="tablist">',
472-
' <a id="tab1" href="#x-tab1" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-tab1">Tab 1</a>',
473-
' <a href="#x-tab2" class="nav-link active" data-bs-toggle="tab" role="tab" aria-controls="x-tab2" aria-selected="true">Tab 2</a>',
474-
' <a href="#x-tab3" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-tab3">Tab 3</a>',
496+
' <button type="button" id="tab1" data-bs-target="#x-tab1" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-tab1">Tab 1</button>',
497+
' <button type="button" data-bs-target="#x-tab2" class="nav-link active" data-bs-toggle="tab" role="tab" aria-controls="x-tab2" aria-selected="true">Tab 2</button>',
498+
' <button type="button" data-bs-target="#x-tab3" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-tab3">Tab 3</button>',
475499
'</nav>',
476500
'<div class="tab-content">',
477501
' <div class="tab-pane" id="x-tab1" role="tabpanel">',
478502
' <nav class="nav nav-tabs" role="tablist">',
479-
' <a href="#nested-tab1" class="nav-link active" data-bs-toggle="tab" role="tab" aria-controls="x-tab1" aria-selected="true">Nested Tab 1</a>',
480-
' <a id="tabNested2" href="#nested-tab2" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-profile">Nested Tab2</a>',
503+
' <button type="button" data-bs-target="#nested-tab1" class="nav-link active" data-bs-toggle="tab" role="tab" aria-controls="x-tab1" aria-selected="true">Nested Tab 1</button>',
504+
' <button type="button" id="tabNested2" data-bs-target="#nested-tab2" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-profile">Nested Tab2</button>',
481505
' </nav>',
482506
' <div class="tab-content">',
483507
' <div class="tab-pane active" id="nested-tab1" role="tabpanel">Nested Tab1 Content</div>',
@@ -509,8 +533,8 @@ describe('Tab', () => {
509533
it('should not remove fade class if no active pane is present', done => {
510534
fixtureEl.innerHTML = [
511535
'<ul class="nav nav-tabs" role="tablist">',
512-
' <li class="nav-item" role="presentation"><a id="tab-home" href="#home" class="nav-link" data-bs-toggle="tab" role="tab">Home</a></li>',
513-
' <li class="nav-item" role="presentation"><a id="tab-profile" href="#profile" class="nav-link" data-bs-toggle="tab" role="tab">Profile</a></li>',
536+
' <li class="nav-item" role="presentation"><button type="button" id="tab-home" data-bs-target="#home" class="nav-link" data-bs-toggle="tab" role="tab">Home</button></li>',
537+
' <li class="nav-item" role="presentation"><button type="button" id="tab-profile" data-bs-target="#profile" class="nav-link" data-bs-toggle="tab" role="tab">Profile</button></li>',
514538
'</ul>',
515539
'<div class="tab-content">',
516540
' <div class="tab-pane fade" id="home" role="tabpanel"></div>',
@@ -547,10 +571,10 @@ describe('Tab', () => {
547571
fixtureEl.innerHTML = [
548572
'<ul class="nav nav-tabs" role="tablist">',
549573
' <li class="nav-item" role="presentation">',
550-
' <a class="nav-link nav-tab" href="#home" role="tab" data-bs-toggle="tab">Home</a>',
574+
' <button type="button" class="nav-link nav-tab" data-bs-target="#home" role="tab" data-bs-toggle="tab">Home</button>',
551575
' </li>',
552576
' <li class="nav-item" role="presentation">',
553-
' <a id="secondNav" class="nav-link nav-tab" href="#profile" role="tab" data-bs-toggle="tab">Profile</a>',
577+
' <button type="button" id="secondNav" class="nav-link nav-tab" data-bs-target="#profile" role="tab" data-bs-toggle="tab">Profile</button>',
554578
' </li>',
555579
'</ul>',
556580
'<div class="tab-content">',
@@ -573,10 +597,10 @@ describe('Tab', () => {
573597
fixtureEl.innerHTML = [
574598
'<ul class="nav nav-tabs" role="tablist">',
575599
' <li class="nav-item" role="presentation">',
576-
' <a class="nav-link nav-tab" href="#home" role="tab" data-bs-toggle="tab">Home</a>',
600+
' <button type="button" class="nav-link nav-tab" data-bs-target="#home" role="tab" data-bs-toggle="tab">Home</button>',
577601
' </li>',
578602
' <li class="nav-item" role="presentation">',
579-
' <a id="secondNav" class="nav-link nav-tab" href="#profile" role="tab" data-bs-toggle="tab">Profile</a>',
603+
' <button type="button" id="secondNav" class="nav-link nav-tab" data-bs-target="#profile" role="tab" data-bs-toggle="tab">Profile</button>',
580604
' </li>',
581605
'</ul>',
582606
'<div class="tab-content">',

0 commit comments

Comments
 (0)