From c68c422d4213221e72660f28adc88f29c31c678c Mon Sep 17 00:00:00 2001 From: kyletsang <6854874+kyletsang@users.noreply.github.com> Date: Sat, 22 Apr 2023 22:25:19 -0700 Subject: [PATCH] Support `Home` and `End` keys in tabs --- js/src/tab.js | 15 +- js/tests/unit/tab.spec.js | 170 ++++++++++++++++++ site/content/docs/5.3/components/navs-tabs.md | 2 +- 3 files changed, 183 insertions(+), 4 deletions(-) diff --git a/js/src/tab.js b/js/src/tab.js index d9993d56e8df..f27bb30cfe25 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,21 @@ 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) + + let nextActiveElement + const children = this._getChildren().filter(element => !isDisabled(element)) + 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..9354bdb500a7 100644 --- a/js/tests/unit/tab.spec.js +++ b/js/tests/unit/tab.spec.js @@ -630,6 +630,108 @@ describe('Tab', () => { expect(spyPrevent).toHaveBeenCalledTimes(2) }) + it('if keydown event is Home, handle it', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const tabEl1 = fixtureEl.querySelector('#tab1') + const tabEl2 = fixtureEl.querySelector('#tab2') + const tabEl3 = fixtureEl.querySelector('#tab3') + const tab1 = new Tab(tabEl1) + const tab2 = new Tab(tabEl2) + const tab3 = new Tab(tabEl3) + const spyShow1 = spyOn(tab1, 'show').and.callThrough() + const spyFocus1 = spyOn(tabEl1, 'focus').and.callThrough() + const spyShow2 = spyOn(tab2, 'show').and.callThrough() + const spyFocus2 = spyOn(tabEl2, 'focus').and.callThrough() + const spyShow3 = spyOn(tab3, 'show').and.callThrough() + const spyFocus3 = spyOn(tabEl3, 'focus').and.callThrough() + + const spyStop = spyOn(Event.prototype, 'stopPropagation').and.callThrough() + const spyPrevent = spyOn(Event.prototype, 'preventDefault').and.callThrough() + + let keydown = createEvent('keydown') + keydown.key = 'Home' + + tabEl3.dispatchEvent(keydown) + expect(spyShow1).toHaveBeenCalled() + expect(spyFocus1).toHaveBeenCalled() + expect(spyShow2).not.toHaveBeenCalled() + expect(spyFocus2).not.toHaveBeenCalled() + expect(spyShow3).not.toHaveBeenCalled() + expect(spyFocus3).not.toHaveBeenCalled() + + keydown = createEvent('keydown') + keydown.key = 'Home' + + tabEl1.dispatchEvent(keydown) + expect(spyShow1).toHaveBeenCalled() + expect(spyFocus1).toHaveBeenCalled() + expect(spyShow2).not.toHaveBeenCalled() + expect(spyFocus2).not.toHaveBeenCalled() + expect(spyShow3).not.toHaveBeenCalled() + expect(spyFocus3).not.toHaveBeenCalled() + + expect(spyStop).toHaveBeenCalledTimes(2) + expect(spyPrevent).toHaveBeenCalledTimes(2) + }) + + it('if keydown event is End, handle it', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const tabEl1 = fixtureEl.querySelector('#tab1') + const tabEl2 = fixtureEl.querySelector('#tab2') + const tabEl3 = fixtureEl.querySelector('#tab3') + const tab1 = new Tab(tabEl1) + const tab2 = new Tab(tabEl2) + const tab3 = new Tab(tabEl3) + const spyShow1 = spyOn(tab1, 'show').and.callThrough() + const spyFocus1 = spyOn(tabEl1, 'focus').and.callThrough() + const spyShow2 = spyOn(tab2, 'show').and.callThrough() + const spyFocus2 = spyOn(tabEl2, 'focus').and.callThrough() + const spyShow3 = spyOn(tab3, 'show').and.callThrough() + const spyFocus3 = spyOn(tabEl3, 'focus').and.callThrough() + + const spyStop = spyOn(Event.prototype, 'stopPropagation').and.callThrough() + const spyPrevent = spyOn(Event.prototype, 'preventDefault').and.callThrough() + + let keydown = createEvent('keydown') + keydown.key = 'End' + + tabEl1.dispatchEvent(keydown) + expect(spyShow1).not.toHaveBeenCalled() + expect(spyFocus1).not.toHaveBeenCalled() + expect(spyShow2).not.toHaveBeenCalled() + expect(spyFocus2).not.toHaveBeenCalled() + expect(spyShow3).toHaveBeenCalled() + expect(spyFocus3).toHaveBeenCalled() + + keydown = createEvent('keydown') + keydown.key = 'End' + + tabEl3.dispatchEvent(keydown) + expect(spyShow1).not.toHaveBeenCalled() + expect(spyFocus1).not.toHaveBeenCalled() + expect(spyShow2).not.toHaveBeenCalled() + expect(spyFocus2).not.toHaveBeenCalled() + expect(spyShow3).toHaveBeenCalled() + expect(spyFocus3).toHaveBeenCalled() + + expect(spyStop).toHaveBeenCalledTimes(2) + expect(spyPrevent).toHaveBeenCalledTimes(2) + }) + it('if keydown event is right arrow and next element is disabled', () => { fixtureEl.innerHTML = [ '