Skip to content
GitLab
    • Explore Projects Groups Snippets
Projects Groups Snippets
  • /
  • Help
    • Help
    • Support
    • Community forum
    • Submit feedback
    • Contribute to GitLab
  • Sign in / Register
  • B bootstrap
  • Project information
    • Project information
    • Activity
    • Labels
    • Members
  • Repository
    • Repository
    • Files
    • Commits
    • Branches
    • Tags
    • Contributors
    • Graph
    • Compare
  • Issues 263
    • Issues 263
    • List
    • Boards
    • Service Desk
    • Milestones
  • Merge requests 114
    • Merge requests 114
  • CI/CD
    • CI/CD
    • Pipelines
    • Jobs
    • Schedules
  • Deployments
    • Deployments
    • Environments
    • Releases
  • Packages and registries
    • Packages and registries
    • Package Registry
    • Infrastructure Registry
  • Monitor
    • Monitor
    • Incidents
  • Analytics
    • Analytics
    • Value stream
    • CI/CD
    • Repository
  • Wiki
    • Wiki
  • Snippets
    • Snippets
  • Activity
  • Graph
  • Create a new issue
  • Jobs
  • Commits
  • Issue Boards
Collapse sidebar
  • Bootstrap
  • bootstrap
  • Merge requests
  • !28927

WIP make dynamic tabs follow ARIA 1.1 practices

  • Review changes

  • Download
  • Email patches
  • Plain diff
Closed Patrick H. Lauke requested to merge patrickhlauke-tabs-kbd-automatic-activation into main 6 years ago
  • Overview 0
  • Commits 45
  • Pipelines 0
  • Changes 5

Closes https://github.com/twbs/bootstrap/issues/28918

Compare
  • main (base)

and
  • latest version
    897f4db1
    45 commits, 2 years ago

5 files
+ 357
- 188

    Preferences

    File browser
    Compare changes
j‎s‎
s‎rc‎
tab‎.js‎ +82 -22
te‎sts‎
un‎it‎
tab‎.js‎ +234 -52
vis‎ual‎
tab.‎html‎ +13 -88
site/content/doc‎s/4.3/components‎
list-g‎roup.md‎ +4 -4
nav‎s.md‎ +24 -22
js/src/tab.js
+ 82
- 22
  • View file @ 897f4db1


@@ -29,17 +29,22 @@ const VERSION = '4.3.1'
const DATA_KEY = 'bs.tab'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const ARROW_LEFT_KEYCODE = 37 // KeyboardEvent.which value for left arrow key
const ARROW_UP_KEYCODE = 38 // KeyboardEvent.which value for up arrow key
const ARROW_RIGHT_KEYCODE = 39 // KeyboardEvent.which value for right arrow key
const ARROW_DOWN_KEYCODE = 40 // KeyboardEvent.which value for down arrow key
const Event = {
HIDE: `hide${EVENT_KEY}`,
HIDDEN: `hidden${EVENT_KEY}`,
SHOW: `show${EVENT_KEY}`,
SHOWN: `shown${EVENT_KEY}`,
CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}`
CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}`,
KEYDOWN_DATA_API: `keydown${EVENT_KEY}${DATA_API_KEY}`,
LOAD_DATA_API: `load${EVENT_KEY}${DATA_API_KEY}`
}
const ClassName = {
DROPDOWN_MENU: 'dropdown-menu',
ACTIVE: 'active',
DISABLED: 'disabled',
FADE: 'fade',
@@ -47,15 +52,16 @@ const ClassName = {
}
const Selector = {
DROPDOWN: '.dropdown',
NAV_LIST_GROUP: '.nav, .list-group',
ACTIVE: '.active',
ACTIVE_UL: ':scope > li > .active',
DATA_TOGGLE: '[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',
DROPDOWN_TOGGLE: '.dropdown-toggle',
DROPDOWN_ACTIVE_CHILD: ':scope > .dropdown-menu .active'
TABLIST: '[role="tablist"]'
}
const ORIENTATION_VERTICAL = 'vertical'
const ORIENTATION_HORIZONTAL = 'horizontal'
/**
* ------------------------------------------------------------------------
* Class Definition
@@ -97,6 +103,7 @@ class Tab {
}
let hideEvent = null
this.isTransitioning = true
if (previous) {
hideEvent = EventHandler.trigger(previous, Event.HIDE, {
@@ -129,6 +136,7 @@ class Tab {
EventHandler.trigger(this._element, Event.SHOWN, {
relatedTarget: previous
})
this.isTransitioning = false
}
if (target) {
@@ -175,20 +183,16 @@ class Tab {
if (active) {
active.classList.remove(ClassName.ACTIVE)
const dropdownChild = SelectorEngine.findOne(Selector.DROPDOWN_ACTIVE_CHILD, active.parentNode)
if (dropdownChild) {
dropdownChild.classList.remove(ClassName.ACTIVE)
}
if (active.getAttribute('role') === 'tab') {
active.setAttribute('aria-selected', false)
active.setAttribute('tabindex', '-1')
}
}
element.classList.add(ClassName.ACTIVE)
if (element.getAttribute('role') === 'tab') {
element.setAttribute('aria-selected', true)
element.setAttribute('tabindex', '0')
}
reflow(element)
@@ -197,17 +201,6 @@ class Tab {
element.classList.add(ClassName.SHOW)
}
if (element.parentNode && element.parentNode.classList.contains(ClassName.DROPDOWN_MENU)) {
const dropdownElement = SelectorEngine.closest(element, Selector.DROPDOWN)
if (dropdownElement) {
makeArray(SelectorEngine.find(Selector.DROPDOWN_TOGGLE))
.forEach(dropdown => dropdown.classList.add(ClassName.ACTIVE))
}
element.setAttribute('aria-expanded', true)
}
if (callback) {
callback()
}
@@ -229,6 +222,48 @@ class Tab {
})
}
static _dataApiKeydownHandler(event) {
const tablist = SelectorEngine.closest(event.target, Selector.TABLIST)
let tabListOrientation = tablist.getAttribute('aria-orientation')
if (tabListOrientation !== ORIENTATION_VERTICAL) {
tabListOrientation = ORIENTATION_HORIZONTAL
}
if ((tabListOrientation === ORIENTATION_HORIZONTAL && event.which !== ARROW_LEFT_KEYCODE && event.which !== ARROW_RIGHT_KEYCODE) ||
(tabListOrientation === ORIENTATION_VERTICAL && event.which !== ARROW_UP_KEYCODE && event.which !== ARROW_DOWN_KEYCODE)) {
return
}
event.preventDefault()
event.stopPropagation()
if (this.disabled || this.classList.contains(ClassName.DISABLED)) {
return
}
const tabs = makeArray(SelectorEngine.find(Selector.DATA_TOGGLE, tablist))
let index = tabs.indexOf(event.target)
const tabInstance = Data.getData(tabs[index], DATA_KEY) || new Tab(tabs[index])
if (tabInstance.isTransitioning) {
return
}
// Left / Up
if ((event.which === ARROW_LEFT_KEYCODE || event.which === ARROW_UP_KEYCODE) && index > 0) {
index--
}
// Right / Down
if ((event.which === ARROW_RIGHT_KEYCODE || event.which === ARROW_DOWN_KEYCODE) && index < tabs.length - 1) {
index++
}
tabs[index].focus()
tabs[index].click()
}
static _getInstance(element) {
return Data.getData(element, DATA_KEY)
}
@@ -240,6 +275,31 @@ class Tab {
* ------------------------------------------------------------------------
*/
EventHandler.on(window, Event.LOAD_DATA_API, () => {
makeArray(SelectorEngine.find(Selector.TABLIST))
.forEach(tablist => {
const tabs = makeArray(SelectorEngine.find(Selector.DATA_TOGGLE, tablist))
let selectedTabFound = false
// iterate over each tab in the tablist, make sure they have correct tabindex/aria-selected
tabs.forEach(tab => {
if (tab.getAttribute('aria-selected') === 'true' && !selectedTabFound) {
tab.setAttribute('tabindex', '0')
selectedTabFound = true
} else {
tab.setAttribute('tabindex', '-1')
tab.setAttribute('aria-selected', 'false')
}
})
// if none of the tabs were explicitly marked as selected, pick first one
if (!selectedTabFound) {
tabs[0].setAttribute('tabindex', '0')
tabs[0].setAttribute('aria-selected', 'true')
}
})
})
EventHandler.on(document, Event.KEYDOWN_DATA_API, Selector.DATA_TOGGLE, Tab._dataApiKeydownHandler)
EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
event.preventDefault()
js/tests/unit/tab.js
+ 234
- 52
  • View file @ 897f4db1


@@ -180,35 +180,6 @@ $(function () {
.bootstrapTab('show')
})
QUnit.test('show and shown events should reference correct relatedTarget', function (assert) {
assert.expect(2)
var done = assert.async()
var dropHTML =
'<ul class="drop nav">' +
' <li class="dropdown"><a data-toggle="dropdown" href="#">1</a>' +
' <ul class="dropdown-menu nav">' +
' <li><a href="#a1-1" data-toggle="tab">1-1</a></li>' +
' <li><a href="#a1-2" data-toggle="tab">1-2</a></li>' +
' </ul>' +
' </li>' +
'</ul>'
$(dropHTML)
.find('ul > li:first-child a')
.bootstrapTab('show')
.end()
.find('ul > li:last-child a')
.on('show.bs.tab', function (e) {
assert.strictEqual(e.relatedTarget.hash, '#a1-1', 'references correct element as relatedTarget')
})
.on('shown.bs.tab', function (e) {
assert.strictEqual(e.relatedTarget.hash, '#a1-1', 'references correct element as relatedTarget')
done()
})
.bootstrapTab('show')
})
QUnit.test('should fire hide and hidden events', function (assert) {
assert.expect(2)
var done = assert.async()
@@ -265,6 +236,30 @@ $(function () {
.bootstrapTab('show')
})
QUnit.test('show and shown events should reference correct relatedTarget', function (assert) {
assert.expect(2)
var done = assert.async()
var tabsHTML = '<ul class="nav">' +
'<li><a href="#home">Home</a></li>' +
'<li><a href="#profile">Profile</a></li>' +
'</ul>'
$(tabsHTML)
.find('li:first-child a')
.bootstrapTab('show')
.end()
.find('li:last-child a')
.on('show.bs.tab', function (e) {
assert.strictEqual(e.relatedTarget.hash, '#home', 'references correct element as relatedTarget')
})
.on('shown.bs.tab', function (e) {
assert.strictEqual(e.relatedTarget.hash, '#home', 'references correct element as relatedTarget')
done()
})
.bootstrapTab('show')
})
QUnit.test('hide and hidden events contain correct relatedTarget', function (assert) {
assert.expect(2)
var done = assert.async()
@@ -291,9 +286,9 @@ $(function () {
QUnit.test('selected tab should have aria-selected', function (assert) {
assert.expect(8)
var tabsHTML = '<ul class="nav nav-tabs">' +
'<li><a class="nav-item active" href="#home" toggle="tab" aria-selected="true">Home</a></li>' +
'<li><a class="nav-item" href="#profile" toggle="tab" aria-selected="false">Profile</a></li>' +
var tabsHTML = '<ul class="nav nav-tabs" role="tablist">' +
'<li><a class="nav-item active" href="#home" data-toggle="tab" role="tab" aria-selected="true">Home</a></li>' +
'<li><a class="nav-item" href="#profile" data-toggle="tab" role="tab" aria-selected="false">Profile</a></li>' +
'</ul>'
var $tabs = $(tabsHTML).appendTo('#qunit-fixture')
@@ -327,26 +322,6 @@ $(function () {
assert.ok($tabs.find('li:last-child a').hasClass('active'))
})
QUnit.test('selected tab should deactivate previous selected link in dropdown', function (assert) {
assert.expect(3)
var tabsHTML = '<ul class="nav nav-tabs">' +
'<li class="nav-item"><a class="nav-link" href="#home" data-toggle="tab">Home</a></li>' +
'<li class="nav-item"><a class="nav-link" href="#profile" data-toggle="tab">Profile</a></li>' +
'<li class="nav-item dropdown"><a class="nav-link dropdown-toggle active" data-toggle="dropdown" href="#">Dropdown</a>' +
'<div class="dropdown-menu">' +
'<a class="dropdown-item active" href="#dropdown1" id="dropdown1-tab" data-toggle="tab">@fat</a>' +
'<a class="dropdown-item" href="#dropdown2" id="dropdown2-tab" data-toggle="tab">@mdo</a>' +
'</div>' +
'</li>' +
'</ul>'
var $tabs = $(tabsHTML).appendTo('#qunit-fixture')
$tabs.find('li:first-child a')[0].click()
assert.ok($tabs.find('li:first-child a').hasClass('active'))
assert.notOk($tabs.find('li:last-child a').hasClass('active'))
assert.notOk($tabs.find('li:last-child .dropdown-menu a:first-child').hasClass('active'))
})
QUnit.test('Nested tabs', function (assert) {
assert.expect(2)
var done = assert.async()
@@ -523,6 +498,213 @@ $(function () {
$('#secondNav')[0].click()
})
QUnit.test('should initialize tab list: only one tab control focusable and aria-selected', function (assert) {
assert.expect(5)
var done = assert.async()
var tabsHTML = '<ul class="nav nav-tabs" role="tablist">' +
'<li><a class="nav-item active" href="#home" data-toggle="tab" role="tab" aria-selected="true">Home</a></li>' +
'<li><a class="nav-item" href="#profile" data-toggle="tab" role="tab" aria-selected="true">Profile</a></li>' +
'<li><a class="nav-item" href="#contact" data-toggle="tab" role="tab" aria-selected="true">Contact</a></li>' +
'</ul>' /* purposely incorrect, with all set to aria-selected="true" */
var $tabs = $(tabsHTML).appendTo('#qunit-fixture')
window.dispatchEvent(new Event('load'))
setTimeout(function () {
assert.strictEqual($tabs.find('li:first-child a').attr('tabindex'), '0', 'first found aria-selected="true" control has been given tabindex="0"')
assert.strictEqual($tabs.find('li:nth-child(2) a').attr('tabindex'), '-1', 'second control has been given tabindex="-1"')
assert.strictEqual($tabs.find('li:nth-child(2) a').attr('aria-selected'), 'false', 'second control has been given aria-selected="false"')
assert.strictEqual($tabs.find('li:nth-child(3) a').attr('tabindex'), '-1', 'third control has been given tabindex="-1"')
assert.strictEqual($tabs.find('li:nth-child(3) a').attr('aria-selected'), 'false', 'third control has been given aria-selected="false"')
done()
}, 10)
})
QUnit.test('should initialize tab list: if no tab control has aria-selected="true", set it to the first one', function (assert) {
assert.expect(3)
var done = assert.async()
var tabsHTML = '<ul class="nav nav-tabs" role="tablist">' +
'<li><a class="nav-item active" href="#home" data-toggle="tab">Home</a></li>' +
'<li><a class="nav-item" href="#profile" data-toggle="tab">Profile</a></li>' +
'<li><a class="nav-item" href="#contact" data-toggle="tab">Contact</a></li>' +
'</ul>' /* purposely incorrect, left out aria-selected="true" on active one */
var $tabs = $(tabsHTML).appendTo('#qunit-fixture')
window.dispatchEvent(new Event('load'))
setTimeout(function () {
assert.strictEqual($tabs.find('li:first-child a').attr('aria-selected'), 'true', 'first control has been given aria-selected="true"')
assert.strictEqual($tabs.find('li:nth-child(2) a').attr('aria-selected'), 'false', 'second control has been given aria-selected="false"')
assert.strictEqual($tabs.find('li:nth-child(3) a').attr('aria-selected'), 'false', 'third control has been given aria-selected="false"')
done()
}, 10)
})
QUnit.test('should correctly switch tabs in response to left/right cursor keys (for horizontal/default tablist) and ignore up/down cursor keys', function (assert) {
assert.expect(27)
var done = assert.async()
var tabsHTML = '<ul class="nav nav-tabs" role="tablist">' +
'<li><a class="nav-item active" href="#home" data-toggle="tab" role="tab" tabindex="0" aria-selected="true">Home</a></li>' +
'<li><a class="nav-item" href="#profile" data-toggle="tab" role="tab" tabindex="-1" aria-selected="false">Profile</a></li>' +
'<li><a class="nav-item" href="#contact" data-toggle="tab" role="tab" tabindex="-1" aria-selected="false">Contact</a></li>' +
'</ul>' +
'<div class="tab-pane show active" id="home" role="tabpanel">...</div>' +
'<div class="tab-pane" id="profile" role="tabpanel">...</div>' +
'<div class="tab-pane" id="contact" role="tabpanel">...</div>'
var $tabs = $(tabsHTML).appendTo('#qunit-fixture')
var $firstTab = $tabs.find('li:first-child a')
var $secondTab = $tabs.find('li:nth-child(2) a')
var $thirdTab = $tabs.find('li:nth-child(3) a')
$firstTab[0].focus() /* set focus to first tab */
assert.ok(document.activeElement === $firstTab[0], 'first tab successfully focused')
/* right cursor key */
var keyDown = new Event('keydown')
keyDown.which = 39
$firstTab[0].dispatchEvent(keyDown)
assert.ok(document.activeElement !== $firstTab[0], 'after 1st right cursor key, first tab not focused anymore')
assert.ok(!$firstTab.hasClass('active'), 'after 1st right cursor key, first tab not `.active` anymore')
assert.ok($firstTab.attr('aria-selected') === 'false', 'after 1st right cursor key, first tab has aria-selected="false"')
assert.ok($firstTab.attr('tabindex') === '-1', 'after 1st right cursor key, first tab has tabindex="-1"')
assert.ok(document.activeElement === $secondTab[0], 'after 1st right cursor key, second tab is focused')
assert.ok($secondTab.hasClass('active'), 'after 1st right cursor key, second tab is `.active`')
assert.ok($secondTab.attr('aria-selected') === 'true', 'after 1st right cursor key, second tab has aria-selected="true"')
assert.ok($secondTab.attr('tabindex') === '0', 'after 1st right cursor key, second tab has tabindex="0"')
/* right cursor key */
$secondTab[0].dispatchEvent(keyDown)
assert.ok(document.activeElement !== $secondTab[0], 'after 2nd right cursor key, second tab not focused anymore')
assert.ok(!$secondTab.hasClass('active'), 'after 2nd right cursor key, second tab not `.active` anymore')
assert.ok($secondTab.attr('aria-selected') === 'false', 'after 2nd right cursor key, second tab has aria-selected="false"')
assert.ok($secondTab.attr('tabindex') === '-1', 'after 2nd right cursor key, second tab has tabindex="-1"')
assert.ok(document.activeElement === $thirdTab[0], 'after 2nd right cursor key, third tab is focused')
assert.ok($thirdTab.hasClass('active'), 'after 2nd right cursor key, third tab is `.active`')
assert.ok($thirdTab.attr('aria-selected') === 'true', 'after 2nd right cursor key, third tab has aria-selected="true"')
assert.ok($thirdTab.attr('tabindex') === '0', 'after 2nd right cursor key, third tab has tabindex="0"')
/* left cursor key */
keyDown.which = 37
$thirdTab[0].dispatchEvent(keyDown)
assert.ok(document.activeElement !== $thirdTab[0], 'after left cursor key, third tab not focused anymore')
assert.ok(!$thirdTab.hasClass('active'), 'after left cursor key, third tab not `.active` anymore')
assert.ok($thirdTab.attr('aria-selected') === 'false', 'after left cursor key, third tab has aria-selected="false"')
assert.ok($thirdTab.attr('tabindex') === '-1', 'after left cursor key, third tab has tabindex="-1"')
assert.ok(document.activeElement === $secondTab[0], 'after left cursor key, second tab is focused')
assert.ok($secondTab.hasClass('active'), 'after left cursor key, second tab is `.active`')
assert.ok($secondTab.attr('aria-selected') === 'true', 'after left cursor key, second tab has aria-selected="true"')
assert.ok($secondTab.attr('tabindex') === '0', 'after left cursor key, second tab has tabindex="0"')
/* up cursor key */
keyDown.which = 38
$secondTab[0].dispatchEvent(keyDown)
assert.ok(document.activeElement === $secondTab[0], 'after up cursor key, second tab is still focused - no change')
/* down cursor key */
keyDown.which = 40
$secondTab[0].dispatchEvent(keyDown)
assert.ok(document.activeElement === $secondTab[0], 'after down cursor key, second tab is still focused - no change')
done()
})
QUnit.test('should correctly switch tabs in response to up/down cursor keys for vertical tablist and ignore left/right cursor keys', function (assert) {
assert.expect(27)
var done = assert.async()
var tabsHTML = '<ul class="nav nav-tabs" role="tablist" aria-orientation="vertical">' +
'<li><a class="nav-item active" href="#home" data-toggle="tab" role="tab" tabindex="0" aria-selected="true">Home</a></li>' +
'<li><a class="nav-item" href="#profile" data-toggle="tab" role="tab" tabindex="-1" aria-selected="false">Profile</a></li>' +
'<li><a class="nav-item" href="#contact" data-toggle="tab" role="tab" tabindex="-1" aria-selected="false">Contact</a></li>' +
'</ul>' +
'<div class="tab-pane show active" id="home" role="tabpanel">...</div>' +
'<div class="tab-pane" id="profile" role="tabpanel">...</div>' +
'<div class="tab-pane" id="contact" role="tabpanel">...</div>'
var $tabs = $(tabsHTML).appendTo('#qunit-fixture')
var $firstTab = $tabs.find('li:first-child a')
var $secondTab = $tabs.find('li:nth-child(2) a')
var $thirdTab = $tabs.find('li:nth-child(3) a')
$firstTab[0].focus() /* set focus to first tab */
assert.ok(document.activeElement === $firstTab[0], 'first tab successfully focused')
/* down cursor key */
var keyDown = new Event('keydown')
keyDown.which = 40
$firstTab[0].dispatchEvent(keyDown)
assert.ok(document.activeElement !== $firstTab[0], 'after 1st down cursor key, first tab not focused anymore')
assert.ok(!$firstTab.hasClass('active'), 'after 1st down cursor key, first tab not `.active` anymore')
assert.ok($firstTab.attr('aria-selected') === 'false', 'after 1st down cursor key, first tab has aria-selected="false"')
assert.ok($firstTab.attr('tabindex') === '-1', 'after 1st down cursor key, first tab has tabindex="-1"')
assert.ok(document.activeElement === $secondTab[0], 'after 1st down cursor key, second tab is focused')
assert.ok($secondTab.hasClass('active'), 'after 1st down cursor key, second tab is `.active`')
assert.ok($secondTab.attr('aria-selected') === 'true', 'after 1st down cursor key, second tab has aria-selected="true"')
assert.ok($secondTab.attr('tabindex') === '0', 'after 1st down cursor key, second tab has tabindex="0"')
/* down cursor key */
$secondTab[0].dispatchEvent(keyDown)
assert.ok(document.activeElement !== $secondTab[0], 'after 2nd down cursor key, second tab not focused anymore')
assert.ok(!$secondTab.hasClass('active'), 'after 2nd down cursor key, second tab not `.active` anymore')
assert.ok($secondTab.attr('aria-selected') === 'false', 'after 2nd down cursor key, second tab has aria-selected="false"')
assert.ok($secondTab.attr('tabindex') === '-1', 'after 2nd down cursor key, second tab has tabindex="-1"')
assert.ok(document.activeElement === $thirdTab[0], 'after 2nd down cursor key, third tab is focused')
assert.ok($thirdTab.hasClass('active'), 'after 2nd down cursor key, third tab is `.active`')
assert.ok($thirdTab.attr('aria-selected') === 'true', 'after 2nd down cursor key, third tab has aria-selected="true"')
assert.ok($thirdTab.attr('tabindex') === '0', 'after 2nd down cursor key, third tab has tabindex="0"')
/* up cursor key */
keyDown.which = 38
$thirdTab[0].dispatchEvent(keyDown)
assert.ok(document.activeElement !== $thirdTab[0], 'after up cursor key, third tab not focused anymore')
assert.ok(!$thirdTab.hasClass('active'), 'after up cursor key, third tab not `.active` anymore')
assert.ok($thirdTab.attr('aria-selected') === 'false', 'after up cursor key, third tab has aria-selected="false"')
assert.ok($thirdTab.attr('tabindex') === '-1', 'after up cursor key, third tab has tabindex="-1"')
assert.ok(document.activeElement === $secondTab[0], 'after up cursor key, second tab is focused')
assert.ok($secondTab.hasClass('active'), 'after up cursor key, second tab is `.active`')
assert.ok($secondTab.attr('aria-selected') === 'true', 'after up cursor key, second tab has aria-selected="true"')
assert.ok($secondTab.attr('tabindex') === '0', 'after up cursor key, second tab has tabindex="0"')
/* left cursor key */
keyDown.which = 37
$secondTab[0].dispatchEvent(keyDown)
assert.ok(document.activeElement === $secondTab[0], 'after left cursor key, second tab is still focused - no change')
/* right cursor key */
keyDown.which = 39
$secondTab[0].dispatchEvent(keyDown)
assert.ok(document.activeElement === $secondTab[0], 'after right cursor key, second tab is still focused - no change')
done()
})
QUnit.test('should not switch tabs in response to cursor keys if tab panel still transitioning', function (assert) {
assert.expect(13)
var done = assert.async()
var tabsHTML = '<ul class="nav nav-tabs" role="tablist">' +
'<li><a class="nav-item active" href="#home" data-toggle="tab" role="tab" tabindex="0" aria-selected="true">Home</a></li>' +
'<li><a class="nav-item" href="#profile" data-toggle="tab" role="tab" tabindex="-1" aria-selected="false">Profile</a></li>' +
'<li><a class="nav-item" href="#contact" data-toggle="tab" role="tab" tabindex="-1" aria-selected="false">Contact</a></li>' +
'</ul>' +
'<div class="tab-pane fade show active" id="home" role="tabpanel">...</div>' +
'<div class="tab-pane fade" id="profile" role="tabpanel">...</div>' +
'<div class="tab-pane fade" id="contact" role="tabpanel">...</div>'
var $tabs = $(tabsHTML).appendTo('#qunit-fixture')
var $firstTab = $tabs.find('li:first-child a')
var $secondTab = $tabs.find('li:nth-child(2) a')
$firstTab[0].focus() /* set focus to first tab */
assert.ok(document.activeElement === $firstTab[0], 'first tab successfully focused')
/* right cursor key */
var keyDown = new Event('keydown')
keyDown.which = 39
$firstTab[0].dispatchEvent(keyDown)
assert.ok(document.activeElement !== $firstTab[0], 'after 1st right cursor key, first tab not focused anymore')
assert.ok(!$firstTab.hasClass('active'), 'after 1st right cursor key, first tab not `.active` anymore')
assert.ok($firstTab.attr('aria-selected') === 'false', 'after 1st right cursor key, first tab has aria-selected="false"')
assert.ok($firstTab.attr('tabindex') === '-1', 'after 1st right cursor key, first tab has tabindex="-1"')
assert.ok(document.activeElement === $secondTab[0], 'after 1st right cursor key, second tab is focused')
assert.ok($secondTab.hasClass('active'), 'after 1st right cursor key, second tab is `.active`')
assert.ok($secondTab.attr('aria-selected') === 'true', 'after 1st right cursor key, second tab has aria-selected="true"')
assert.ok($secondTab.attr('tabindex') === '0', 'after 1st right cursor key, second tab has tabindex="0"')
/* right cursor key - as this is fired so quickly, the tab panel is still transitioning */
$secondTab[0].dispatchEvent(keyDown)
assert.ok(document.activeElement === $secondTab[0], 'after 2nd right cursor key, while tab panel still transitioning, second tab is still focused')
assert.ok($secondTab.hasClass('active'), 'after 2nd right cursor key, while tab panel still transitioning, second tab is still `.active`')
assert.ok($secondTab.attr('aria-selected') === 'true', 'after 2nd right cursor key, while tab panel still transitioning, second tab still has aria-selected="true"')
assert.ok($secondTab.attr('tabindex') === '0', 'after 2nd right cursor key, while tab panel still transitioning, second tab still has tabindex="0"')
done()
})
QUnit.test('should return the version', function (assert) {
assert.expect(1)
assert.strictEqual(typeof Tab.VERSION, 'string')
js/tests/visual/tab.html
+ 13
- 88
  • View file @ 897f4db1

Files with large changes are collapsed by default.

site/content/docs/4.3/components/list-group.md
+ 4
- 4
  • View file @ 897f4db1


@@ -205,7 +205,7 @@ Use the tab JavaScript plugin—include it individually or through the compiled
<div class="bd-example" role="tabpanel">
<div class="row">
<div class="col-4">
<div class="list-group" id="list-tab" role="tablist">
<div class="list-group" id="list-tab" role="tablist" aria-orientation="vertical">
<a class="list-group-item list-group-item-action active" id="list-home-list" data-toggle="tab" href="#list-home" role="tab" aria-controls="list-home">Home</a>
<a class="list-group-item list-group-item-action" id="list-profile-list" data-toggle="tab" href="#list-profile" role="tab" aria-controls="list-profile">Profile</a>
<a class="list-group-item list-group-item-action" id="list-messages-list" data-toggle="tab" href="#list-messages" role="tab" aria-controls="list-messages">Messages</a>
@@ -234,7 +234,7 @@ Use the tab JavaScript plugin—include it individually or through the compiled
{{< highlight html >}}
<div class="row">
<div class="col-4">
<div class="list-group" id="list-tab" role="tablist">
<div class="list-group" id="list-tab" role="tablist" aria-orientation="vertical">
<a class="list-group-item list-group-item-action active" id="list-home-list" data-toggle="list" href="#list-home" role="tab" aria-controls="home">Home</a>
<a class="list-group-item list-group-item-action" id="list-profile-list" data-toggle="list" href="#list-profile" role="tab" aria-controls="profile">Profile</a>
<a class="list-group-item list-group-item-action" id="list-messages-list" data-toggle="list" href="#list-messages" role="tab" aria-controls="messages">Messages</a>
@@ -259,7 +259,7 @@ You can activate a list group navigation without writing any JavaScript by simpl
<div role="tabpanel">
{{< highlight html >}}
<!-- List group -->
<div class="list-group" id="myList" role="tablist">
<div class="list-group" id="myList" role="tablist" aria-orientation="vertical">
<a class="list-group-item list-group-item-action active" data-toggle="list" href="#home" role="tab">Home</a>
<a class="list-group-item list-group-item-action" data-toggle="list" href="#profile" role="tab">Profile</a>
<a class="list-group-item list-group-item-action" data-toggle="list" href="#messages" role="tab">Messages</a>
@@ -316,7 +316,7 @@ To make tabs panel fade in, add `.fade` to each `.tab-pane`. The first tab pane
Activates a list item element and content container. Tab should have either a `data-target` or an `href` targeting a container node in the DOM.
{{< highlight html >}}
<div class="list-group" id="myList" role="tablist">
<div class="list-group" id="myList" role="tablist" aria-orientation="vertical">
<a class="list-group-item list-group-item-action active" data-toggle="list" href="#home" role="tab">Home</a>
<a class="list-group-item list-group-item-action" data-toggle="list" href="#profile" role="tab">Profile</a>
<a class="list-group-item list-group-item-action" data-toggle="list" href="#messages" role="tab">Messages</a>
site/content/docs/4.3/components/navs.md
+ 24
- 22
  • View file @ 897f4db1


@@ -307,17 +307,19 @@ Use the tab JavaScript plugin—include it individually or through the compiled
Dynamic tabbed interfaces, as described in the [<abbr title="Web Accessibility Initiative">WAI</abbr> <abbr title="Accessible Rich Internet Applications">ARIA</abbr> Authoring Practices](https://www.w3.org/TR/wai-aria-practices/#tabpanel), require `role="tablist"`, `role="tab"`, `role="tabpanel"`, and additional `aria-` attributes in order to convey their structure, functionality and current state to users of assistive technologies (such as screen readers).
Note that dynamic tabbed interfaces should <em>not</em> contain dropdown menus, as this causes both usability and accessibility issues. From a usability perspective, the fact that the currently displayed tab's trigger element is not immediately visible (as it's inside the closed dropdown menu) can cause confusion. From an accessibility point of view, there is currently no sensible way to map this sort of construct to a standard WAI ARIA pattern, meaning that it cannot be easily made understandable to users of assistive technologies.
{{< callout warning >}}
Dynamic tabbed interfaces <em>can't</em> contain dropdown menus, as this causes both usability and accessibility issues. From a usability perspective, the fact that the currently displayed tab's trigger element is not immediately visible (as it's inside the closed dropdown menu) can cause confusion. From an accessibility point of view, there is currently no sensible way to map this sort of construct to a standard WAI ARIA pattern, meaning that it cannot be easily made understandable to users of assistive technologies.
{{< /callout >}}
<div class="bd-example">
<ul class="nav nav-tabs mb-3" id="myTab" role="tablist">
<li class="nav-item">
<li class="nav-item" role="presentation">
<a class="nav-link active" id="home-tab" data-toggle="tab" href="#home" role="tab" aria-controls="home" aria-selected="true">Home</a>
</li>
<li class="nav-item">
<li class="nav-item" role="presentation">
<a class="nav-link" id="profile-tab" data-toggle="tab" href="#profile" role="tab" aria-controls="profile" aria-selected="false">Profile</a>
</li>
<li class="nav-item">
<li class="nav-item" role="presentation">
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#contact" role="tab" aria-controls="contact" aria-selected="false">Contact</a>
</li>
</ul>
@@ -336,13 +338,13 @@ Note that dynamic tabbed interfaces should <em>not</em> contain dropdown menus,
{{< highlight html >}}
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item">
<li class="nav-item" role="presentation">
<a class="nav-link active" id="home-tab" data-toggle="tab" href="#home" role="tab" aria-controls="home" aria-selected="true">Home</a>
</li>
<li class="nav-item">
<li class="nav-item" role="presentation">
<a class="nav-link" id="profile-tab" data-toggle="tab" href="#profile" role="tab" aria-controls="profile" aria-selected="false">Profile</a>
</li>
<li class="nav-item">
<li class="nav-item" role="presentation">
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#contact" role="tab" aria-controls="contact" aria-selected="false">Contact</a>
</li>
</ul>
@@ -395,13 +397,13 @@ The tabs plugin also works with pills.
<div class="bd-example">
<ul class="nav nav-pills mb-3" id="pills-tab" role="tablist">
<li class="nav-item">
<li class="nav-item" role="presentation">
<a class="nav-link active" id="pills-home-tab" data-toggle="pill" href="#pills-home" role="tab" aria-controls="pills-home" aria-selected="true">Home</a>
</li>
<li class="nav-item">
<li class="nav-item" role="presentation">
<a class="nav-link" id="pills-profile-tab" data-toggle="pill" href="#pills-profile" role="tab" aria-controls="pills-profile" aria-selected="false">Profile</a>
</li>
<li class="nav-item">
<li class="nav-item" role="presentation">
<a class="nav-link" id="pills-contact-tab" data-toggle="pill" href="#pills-contact" role="tab" aria-controls="pills-contact" aria-selected="false">Contact</a>
</li>
</ul>
@@ -420,13 +422,13 @@ The tabs plugin also works with pills.
{{< highlight html >}}
<ul class="nav nav-pills mb-3" id="pills-tab" role="tablist">
<li class="nav-item">
<li class="nav-item" role="presentation">
<a class="nav-link active" id="pills-home-tab" data-toggle="pill" href="#pills-home" role="tab" aria-controls="pills-home" aria-selected="true">Home</a>
</li>
<li class="nav-item">
<li class="nav-item" role="presentation">
<a class="nav-link" id="pills-profile-tab" data-toggle="pill" href="#pills-profile" role="tab" aria-controls="pills-profile" aria-selected="false">Profile</a>
</li>
<li class="nav-item">
<li class="nav-item" role="presentation">
<a class="nav-link" id="pills-contact-tab" data-toggle="pill" href="#pills-contact" role="tab" aria-controls="pills-contact" aria-selected="false">Contact</a>
</li>
</ul>
@@ -437,7 +439,7 @@ The tabs plugin also works with pills.
</div>
{{< /highlight >}}
And with vertical pills.
When making vertical tab panels, make sure to include `aria-orientation="vertical"` to switch to the appropriate keyboard behavior (switching tabs with the up and down cursor keys).
<div class="bd-example">
<div class="row">
@@ -496,16 +498,16 @@ You can activate a tab or pill navigation without writing any JavaScript by simp
{{< highlight html >}}
<!-- Nav tabs -->
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item">
<li class="nav-item" role="presentation">
<a class="nav-link active" id="home-tab" data-toggle="tab" href="#home" role="tab" aria-controls="home" aria-selected="true">Home</a>
</li>
<li class="nav-item">
<li class="nav-item" role="presentation">
<a class="nav-link" id="profile-tab" data-toggle="tab" href="#profile" role="tab" aria-controls="profile" aria-selected="false">Profile</a>
</li>
<li class="nav-item">
<li class="nav-item" role="presentation">
<a class="nav-link" id="messages-tab" data-toggle="tab" href="#messages" role="tab" aria-controls="messages" aria-selected="false">Messages</a>
</li>
<li class="nav-item">
<li class="nav-item" role="presentation">
<a class="nav-link" id="settings-tab" data-toggle="tab" href="#settings" role="tab" aria-controls="settings" aria-selected="false">Settings</a>
</li>
</ul>
@@ -564,16 +566,16 @@ Activates a tab element and content container. Tab should have either a `data-ta
{{< highlight html >}}
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item">
<li class="nav-item" role="presentation">
<a class="nav-link active" id="home-tab" data-toggle="tab" href="#home" role="tab" aria-controls="home" aria-selected="true">Home</a>
</li>
<li class="nav-item">
<li class="nav-item" role="presentation">
<a class="nav-link" id="profile-tab" data-toggle="tab" href="#profile" role="tab" aria-controls="profile" aria-selected="false">Profile</a>
</li>
<li class="nav-item">
<li class="nav-item" role="presentation">
<a class="nav-link" id="messages-tab" data-toggle="tab" href="#messages" role="tab" aria-controls="messages" aria-selected="false">Messages</a>
</li>
<li class="nav-item">
<li class="nav-item" role="presentation">
<a class="nav-link" id="settings-tab" data-toggle="tab" href="#settings" role="tab" aria-controls="settings" aria-selected="false">Settings</a>
</li>
</ul>
0 Assignees
None
Assign to
0 Reviewers
None
Request review from
Labels
7
awaiting-reply backport-to-v4 has conflicts help wanted js on-hold v5
7
awaiting-reply backport-to-v4 has conflicts help wanted js on-hold v5
    Assign labels
  • Manage project labels

Milestone
No milestone
None
None
Time tracking
No estimate or time spent
Lock merge request
Unlocked
2
2 participants
Patrick H. Lauke
Ghost User
Reference: twbs/bootstrap!28927
Source branch: patrickhlauke-tabs-kbd-automatic-activation

Menu

Explore Projects Groups Snippets