Skip to content

Commit a19ded0

Browse files
committed
feat: add focus management for toolbar
This follows best practices for toolbar focus management, following the guidelines available at https://www.w3.org/TR/wai-aria-practices-1.1/examples/toolbar/toolbar.html. ArrowLeft/ArrowRight will switch tabindex/focus the next and previous buttons respectively, cycling round the whole toolbar if needed. Home and End keys were also implemented.
1 parent 4516834 commit a19ded0

File tree

2 files changed

+96
-2
lines changed

2 files changed

+96
-2
lines changed

index.js

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,33 @@
22

33
function keydown(fn: KeyboardEventHandler): KeyboardEventHandler {
44
return function(event: KeyboardEvent) {
5-
if (event.key === ' ' || event.key === 'Enter') {
5+
const key = event.key
6+
if (key === ' ' || key === 'Enter') {
67
event.preventDefault()
78
fn(event)
89
}
10+
if (key === 'ArrowRight' || key === 'ArrowLeft' || key === 'Home' || key === 'End') {
11+
const target = event.currentTarget
12+
if (!(target instanceof MarkdownButtonElement)) return
13+
const toolbar = target.closest('markdown-toolbar')
14+
if (!(toolbar instanceof MarkdownToolbarElement)) return
15+
16+
const buttons = []
17+
for (const button of toolbar.children) {
18+
if (!(button instanceof MarkdownButtonElement)) continue
19+
button.setAttribute('tabindex', '-1')
20+
buttons.push(button)
21+
}
22+
let i = 0
23+
if (key === 'ArrowLeft') i = buttons.indexOf(target) - 1
24+
if (key === 'ArrowRight') i = buttons.indexOf(target) + 1
25+
if (key === 'End') i = buttons.length - 1
26+
if (i < 0) i = buttons.length - 1
27+
if (i > buttons.length - 1) i = 0
28+
29+
buttons[i].setAttribute('tabindex', '0')
30+
buttons[i].focus()
31+
}
932
}
1033
}
1134

@@ -25,7 +48,7 @@ class MarkdownButtonElement extends HTMLElement {
2548

2649
connectedCallback() {
2750
if (!this.hasAttribute('tabindex')) {
28-
this.setAttribute('tabindex', '0')
51+
this.setAttribute('tabindex', '-1')
2952
}
3053

3154
if (!this.hasAttribute('role')) {
@@ -221,11 +244,16 @@ class MarkdownToolbarElement extends HTMLElement {
221244
}
222245

223246
connectedCallback() {
247+
if (!this.hasAttribute('role')) {
248+
this.setAttribute('role', 'toolbar')
249+
}
224250
const fn = shortcut.bind(null, this)
225251
if (this.field) {
226252
this.field.addEventListener('keydown', fn)
227253
shortcutListeners.set(this, fn)
228254
}
255+
const firstTabIndex = document.querySelector('[role="button"][tabindex]')
256+
if (firstTabIndex) firstTabIndex.setAttribute('tabindex', '0')
229257
}
230258

231259
disconnectedCallback() {

test/test.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,72 @@ describe('markdown-toolbar-element', function() {
9292
document.body.innerHTML = ''
9393
})
9494

95+
describe('focus management', function() {
96+
function focusFirstButton() {
97+
const button = document.querySelector('md-bold')
98+
button.focus()
99+
}
100+
101+
function pushKeyOnFocussedButton(key) {
102+
const event = document.createEvent('Event')
103+
event.initEvent('keydown', true, true)
104+
event.key = key
105+
document.activeElement.dispatchEvent(event)
106+
}
107+
108+
function getElementsWithTabindex(index) {
109+
return [...document.querySelectorAll(`markdown-toolbar [tabindex="${index}"]`)]
110+
}
111+
112+
it('moves focus to next button when ArrowRight is pressed', function() {
113+
focusFirstButton()
114+
pushKeyOnFocussedButton('ArrowRight')
115+
assert.equal(getElementsWithTabindex(-1).length, 13)
116+
assert.deepEqual(getElementsWithTabindex(0), [document.querySelector('md-header')])
117+
assert.deepEqual(getElementsWithTabindex(0), [document.activeElement])
118+
pushKeyOnFocussedButton('ArrowRight')
119+
assert.equal(getElementsWithTabindex(-1).length, 13)
120+
assert.deepEqual(getElementsWithTabindex(0), [document.querySelector('md-header[level="1"]')])
121+
assert.deepEqual(getElementsWithTabindex(0), [document.activeElement])
122+
pushKeyOnFocussedButton('ArrowRight')
123+
assert.equal(getElementsWithTabindex(-1).length, 13)
124+
assert.deepEqual(getElementsWithTabindex(0), [document.querySelector('md-header[level="10"]')])
125+
assert.deepEqual(getElementsWithTabindex(0), [document.activeElement])
126+
})
127+
128+
it('cycles focus round to last element from first when ArrowLeft is pressed', function() {
129+
focusFirstButton()
130+
pushKeyOnFocussedButton('ArrowLeft')
131+
assert.equal(getElementsWithTabindex(-1).length, 13)
132+
assert.deepEqual(getElementsWithTabindex(0), [document.querySelector('md-ref')])
133+
assert.deepEqual(getElementsWithTabindex(0), [document.activeElement])
134+
pushKeyOnFocussedButton('ArrowLeft')
135+
assert.equal(getElementsWithTabindex(-1).length, 13)
136+
assert.deepEqual(getElementsWithTabindex(0), [document.querySelector('md-mention')])
137+
assert.deepEqual(getElementsWithTabindex(0), [document.activeElement])
138+
})
139+
140+
it('focussed first/last button when Home/End key is pressed', function() {
141+
focusFirstButton()
142+
pushKeyOnFocussedButton('End')
143+
assert.equal(getElementsWithTabindex(-1).length, 13)
144+
assert.deepEqual(getElementsWithTabindex(0), [document.querySelector('md-ref')])
145+
assert.deepEqual(getElementsWithTabindex(0), [document.activeElement])
146+
pushKeyOnFocussedButton('End')
147+
assert.equal(getElementsWithTabindex(-1).length, 13)
148+
assert.deepEqual(getElementsWithTabindex(0), [document.querySelector('md-ref')])
149+
assert.deepEqual(getElementsWithTabindex(0), [document.activeElement])
150+
pushKeyOnFocussedButton('Home')
151+
assert.equal(getElementsWithTabindex(-1).length, 13)
152+
assert.deepEqual(getElementsWithTabindex(0), [document.querySelector('md-bold')])
153+
assert.deepEqual(getElementsWithTabindex(0), [document.activeElement])
154+
pushKeyOnFocussedButton('Home')
155+
assert.equal(getElementsWithTabindex(-1).length, 13)
156+
assert.deepEqual(getElementsWithTabindex(0), [document.querySelector('md-bold')])
157+
assert.deepEqual(getElementsWithTabindex(0), [document.activeElement])
158+
})
159+
})
160+
95161
describe('bold', function() {
96162
it('bold selected text when you click the bold icon', function() {
97163
setVisualValue('The |quick| brown fox jumps over the lazy dog')

0 commit comments

Comments
 (0)