Skip to content

Commit f02c7d3

Browse files
committed
Support [aria-orientation="vertical"]
When the `[role="tablist"]` element declares [`[aria-orientation="vertical"]`][aria-orientation], directional [keyboard navigation changes][keyboard-interaction] from `ArrowRight` and `ArrowLeft` to `ArrowDown` and `ArrowUp`: > When a tab list has its `aria-orientation` set to `vertical`: > * <kbd>Down Arrow</kbd> performs as <kbd>Right Arrow</kbd> is described above. > * <kbd>Up Arrow</kbd> performs as <kbd>Left Arrow</kbd> is described above. [aria-orientation]: https://www.w3.org/TR/wai-aria/#aria-orientation [keyboard-interaction]: https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/#keyboard-interaction-21
1 parent e6a16eb commit f02c7d3

2 files changed

Lines changed: 95 additions & 2 deletions

File tree

src/index.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1+
type IncrementKey = 'ArrowRight' | 'ArrowDown'
2+
type DecrementKey = 'ArrowUp' | 'ArrowLeft'
3+
type NavigationDirection = [IncrementKey, DecrementKey]
4+
15
function getTabs(el: TabContainerElement): HTMLElement[] {
26
return Array.from(el.querySelectorAll<HTMLElement>('[role="tablist"] [role="tab"]')).filter(
37
tab => tab instanceof HTMLElement && tab.closest(el.tagName) === el
48
)
59
}
610

11+
function getNavigationKeys(vertical: boolean): NavigationDirection {
12+
if (vertical) {
13+
return ['ArrowDown', 'ArrowUp']
14+
} else {
15+
return ['ArrowRight', 'ArrowLeft']
16+
}
17+
}
18+
719
export default class TabContainerElement extends HTMLElement {
820
constructor() {
921
super()
@@ -15,12 +27,15 @@ export default class TabContainerElement extends HTMLElement {
1527
if (target.getAttribute('role') !== 'tab' && !target.closest('[role="tablist"]')) return
1628
const tabs = getTabs(this)
1729
const currentIndex = tabs.indexOf(tabs.find(tab => tab.matches('[aria-selected="true"]'))!)
30+
const [incrementKey, decrementKey] = getNavigationKeys(
31+
!!target.closest('[role="tablist"][aria-orientation="vertical"]')
32+
)
1833

19-
if (event.code === 'ArrowRight') {
34+
if (event.code === incrementKey) {
2035
let index = currentIndex + 1
2136
if (index >= tabs.length) index = 0
2237
selectTab(this, index)
23-
} else if (event.code === 'ArrowLeft') {
38+
} else if (event.code === decrementKey) {
2439
let index = currentIndex - 1
2540
if (index < 0) index = tabs.length - 1
2641
selectTab(this, index)

test/test.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,4 +219,82 @@ describe('tab-container', function () {
219219
)
220220
})
221221
})
222+
223+
describe('with [role="tablist"][aria-orientation="vertical"]', function () {
224+
beforeEach(function () {
225+
// eslint-disable-next-line github/no-inner-html
226+
document.body.innerHTML = `
227+
<tab-container>
228+
<div role="tablist" aria-orientation="vertical">
229+
<button type="button" role="tab" aria-selected="true">Tab one</button>
230+
<button type="button" role="tab">Tab two</button>
231+
<button type="button" role="tab">Tab three</button>
232+
</div>
233+
<div role="tabpanel">
234+
Panel 1
235+
</div>
236+
<div role="tabpanel" hidden>
237+
Panel 2
238+
</div>
239+
<div role="tabpanel" hidden data-tab-container-no-tabstop>
240+
Panel 3
241+
</div>
242+
</tab-container>
243+
`
244+
})
245+
246+
it('supports up and down keyboard shortcuts', () => {
247+
const tabContainer = document.querySelector('tab-container')
248+
const tabs = document.querySelectorAll('button')
249+
const panels = document.querySelectorAll('[role="tabpanel"]')
250+
let counter = 0
251+
tabContainer.addEventListener('tab-container-changed', () => counter++)
252+
253+
tabs[0].dispatchEvent(new KeyboardEvent('keydown', {code: 'ArrowUp', bubbles: true}))
254+
assert(panels[0].hidden)
255+
assert(!panels[2].hidden)
256+
assert.equal(document.activeElement, tabs[2])
257+
258+
tabs[0].dispatchEvent(new KeyboardEvent('keydown', {code: 'Home', bubbles: true}))
259+
assert(!panels[0].hidden)
260+
assert(panels[2].hidden)
261+
assert.equal(document.activeElement, tabs[0])
262+
assert.equal(counter, 2)
263+
264+
tabs[0].dispatchEvent(new KeyboardEvent('keydown', {code: 'ArrowDown', bubbles: true}))
265+
assert(panels[0].hidden)
266+
assert(!panels[1].hidden)
267+
assert(panels[2].hidden)
268+
assert.equal(document.activeElement, panels[1])
269+
270+
tabs[1].dispatchEvent(new KeyboardEvent('keydown', {code: 'End', bubbles: true}))
271+
assert(panels[0].hidden)
272+
assert(panels[1].hidden)
273+
assert(!panels[2].hidden)
274+
assert.equal(document.activeElement, tabs[2])
275+
assert.equal(counter, 2)
276+
})
277+
278+
it('does not supports left and right keyboard shortcuts', () => {
279+
const tabContainer = document.querySelector('tab-container')
280+
const tabs = document.querySelectorAll('button')
281+
const panels = document.querySelectorAll('[role="tabpanel"]')
282+
let counter = 0
283+
tabContainer.addEventListener('tab-container-changed', () => counter++)
284+
285+
tabs[0].dispatchEvent(new KeyboardEvent('keydown', {code: 'ArrowLeft', bubbles: true}))
286+
assert(!panels[0].hidden)
287+
assert(panels[1].hidden)
288+
assert(panels[2].hidden)
289+
assert.equal(document.activeElement, tabs[0])
290+
assert.equal(counter, 0)
291+
292+
tabs[0].dispatchEvent(new KeyboardEvent('keydown', {code: 'ArrowRight', bubbles: true}))
293+
assert(!panels[0].hidden)
294+
assert(panels[1].hidden)
295+
assert(panels[2].hidden)
296+
assert.equal(document.activeElement, tabs[0])
297+
assert.equal(counter, 0)
298+
})
299+
})
222300
})

0 commit comments

Comments
 (0)