diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json
index 01ccb8deed7c..4e167c9cc703 100644
--- a/.bundlewatch.config.json
+++ b/.bundlewatch.config.json
@@ -54,7 +54,7 @@
},
{
"path": "./dist/js/bootstrap.min.js",
- "maxSize": "16.1 kB"
+ "maxSize": "16.25 kB"
}
],
"ci": {
diff --git a/js/src/tab.js b/js/src/tab.js
index d9993d56e8df..5598e15c0243 100644
--- a/js/src/tab.js
+++ b/js/src/tab.js
@@ -30,6 +30,8 @@ const ARROW_LEFT_KEY = 'ArrowLeft'
const ARROW_RIGHT_KEY = 'ArrowRight'
const ARROW_UP_KEY = 'ArrowUp'
const ARROW_DOWN_KEY = 'ArrowDown'
+const HOME_KEY = 'Home'
+const END_KEY = 'End'
const CLASS_NAME_ACTIVE = 'active'
const CLASS_NAME_FADE = 'fade'
@@ -151,14 +153,22 @@ class Tab extends BaseComponent {
}
_keydown(event) {
- if (!([ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key))) {
+ if (!([ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key))) {
return
}
event.stopPropagation()// stopPropagation/preventDefault both added to support up/down keys without scrolling the page
event.preventDefault()
- const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key)
- const nextActiveElement = getNextActiveElement(this._getChildren().filter(element => !isDisabled(element)), event.target, isNext, true)
+
+ const children = this._getChildren().filter(element => !isDisabled(element))
+ let nextActiveElement
+
+ if ([HOME_KEY, END_KEY].includes(event.key)) {
+ nextActiveElement = children[event.key === HOME_KEY ? 0 : children.length - 1]
+ } else {
+ const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key)
+ nextActiveElement = getNextActiveElement(children, event.target, isNext, true)
+ }
if (nextActiveElement) {
nextActiveElement.focus({ preventScroll: true })
diff --git a/js/tests/unit/tab.spec.js b/js/tests/unit/tab.spec.js
index 84690fc51fc0..007adddc6014 100644
--- a/js/tests/unit/tab.spec.js
+++ b/js/tests/unit/tab.spec.js
@@ -630,6 +630,58 @@ describe('Tab', () => {
expect(spyPrevent).toHaveBeenCalledTimes(2)
})
+ it('if keydown event is Home, handle it', () => {
+ fixtureEl.innerHTML = [
+ '
',
+ ' ',
+ ' ',
+ ' ',
+ '
'
+ ].join('')
+
+ const tabEl1 = fixtureEl.querySelector('#tab1')
+ const tabEl3 = fixtureEl.querySelector('#tab3')
+
+ const tab3 = new Tab(tabEl3)
+ tab3.show()
+
+ const spyShown = jasmine.createSpy()
+ tabEl1.addEventListener('shown.bs.tab', spyShown)
+
+ const keydown = createEvent('keydown')
+ keydown.key = 'Home'
+
+ tabEl3.dispatchEvent(keydown)
+
+ expect(spyShown).toHaveBeenCalled()
+ })
+
+ it('if keydown event is End, handle it', () => {
+ fixtureEl.innerHTML = [
+ '',
+ ' ',
+ ' ',
+ ' ',
+ '
'
+ ].join('')
+
+ const tabEl1 = fixtureEl.querySelector('#tab1')
+ const tabEl3 = fixtureEl.querySelector('#tab3')
+
+ const tab1 = new Tab(tabEl1)
+ tab1.show()
+
+ const spyShown = jasmine.createSpy()
+ tabEl3.addEventListener('shown.bs.tab', spyShown)
+
+ const keydown = createEvent('keydown')
+ keydown.key = 'End'
+
+ tabEl1.dispatchEvent(keydown)
+
+ expect(spyShown).toHaveBeenCalled()
+ })
+
it('if keydown event is right arrow and next element is disabled', () => {
fixtureEl.innerHTML = [
'',
@@ -711,6 +763,66 @@ describe('Tab', () => {
expect(spyFocus2).not.toHaveBeenCalled()
expect(spyFocus1).toHaveBeenCalledTimes(1)
})
+
+ it('if keydown event is Home and first element is disabled', () => {
+ fixtureEl.innerHTML = [
+ '
',
+ ' ',
+ ' ',
+ ' ',
+ '
'
+ ].join('')
+
+ const tabEl1 = fixtureEl.querySelector('#tab1')
+ const tabEl2 = fixtureEl.querySelector('#tab2')
+ const tabEl3 = fixtureEl.querySelector('#tab3')
+ const tab3 = new Tab(tabEl3)
+
+ tab3.show()
+
+ const spyShown1 = jasmine.createSpy()
+ const spyShown2 = jasmine.createSpy()
+ tabEl1.addEventListener('shown.bs.tab', spyShown1)
+ tabEl2.addEventListener('shown.bs.tab', spyShown2)
+
+ const keydown = createEvent('keydown')
+ keydown.key = 'Home'
+
+ tabEl3.dispatchEvent(keydown)
+
+ expect(spyShown1).not.toHaveBeenCalled()
+ expect(spyShown2).toHaveBeenCalled()
+ })
+
+ it('if keydown event is End and last element is disabled', () => {
+ fixtureEl.innerHTML = [
+ '
',
+ ' ',
+ ' ',
+ ' ',
+ '
'
+ ].join('')
+
+ const tabEl1 = fixtureEl.querySelector('#tab1')
+ const tabEl2 = fixtureEl.querySelector('#tab2')
+ const tabEl3 = fixtureEl.querySelector('#tab3')
+ const tab1 = new Tab(tabEl1)
+
+ tab1.show()
+
+ const spyShown2 = jasmine.createSpy()
+ const spyShown3 = jasmine.createSpy()
+ tabEl2.addEventListener('shown.bs.tab', spyShown2)
+ tabEl3.addEventListener('shown.bs.tab', spyShown3)
+
+ const keydown = createEvent('keydown')
+ keydown.key = 'End'
+
+ tabEl1.dispatchEvent(keydown)
+
+ expect(spyShown3).not.toHaveBeenCalled()
+ expect(spyShown2).toHaveBeenCalled()
+ })
})
describe('jQueryInterface', () => {
diff --git a/site/content/docs/5.3/components/navs-tabs.md b/site/content/docs/5.3/components/navs-tabs.md
index 0a99e34f8fc5..5cf75e145038 100644
--- a/site/content/docs/5.3/components/navs-tabs.md
+++ b/site/content/docs/5.3/components/navs-tabs.md
@@ -567,7 +567,7 @@ And with vertical pills. Ideally, for vertical tabs, you should also add `aria-o
Dynamic tabbed interfaces, as described in the [ARIA Authoring Practices Guide tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/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). As a best practice, we recommend using `