Skip to content

Feat add focus management for toolbar #30

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Apr 2, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4516834
test: add a focussable link ahead of markdown-toolbar in example
keithamus Mar 17, 2020
a19ded0
feat: add focus management for toolbar
keithamus Mar 17, 2020
987b5a2
test: change example link to button
keithamus Mar 18, 2020
72c0c73
feat: use `data-md-button` to select for focus management
keithamus Mar 18, 2020
fc01316
feat: use delegated event listener for keyboard focus
keithamus Mar 18, 2020
bf0fde1
fix: remove MarkDownButtonElement instance check
keithamus Mar 18, 2020
fa869bd
fix: check currentTarget is closest to button invoking keypress
keithamus Mar 18, 2020
95b19a5
refactor: drop unecessary binding on focusKeydown
keithamus Mar 18, 2020
538ff34
refactor: use md-* selectors where possible
keithamus Mar 19, 2020
311e8c6
refactor: move tabIndex assigment to markdown-toolbar
keithamus Mar 19, 2020
054b4c3
test: add test for generic data-md-button elements
keithamus Mar 19, 2020
98ed87f
refactor: DRY up indexOf calls
keithamus Mar 19, 2020
abafcb6
style: drop erroneous console.log
keithamus Mar 19, 2020
f1bfaba
refactor: DRY up buttons.length
keithamus Mar 19, 2020
7fef62f
refactor: move needless if condition out of loop
keithamus Mar 19, 2020
014e98f
style: add return type annotation for getButtons function
keithamus Mar 24, 2020
e6e2ae0
style: add type guard to buttons
keithamus Mar 24, 2020
33a7ceb
refactor: move element selectors to assignment
keithamus Mar 26, 2020
baa5138
fix: filter out hidden elements
keithamus Mar 26, 2020
715efc8
fix: do not focus on buttons that are hidden via CSS
keithamus Mar 27, 2020
85206b2
fix: make focus management lazy, on focus of toolbar.
keithamus Mar 31, 2020
ee95e9d
fix: Home/End shortcuts should preventDefault
keithamus Mar 31, 2020
4d6dbc8
test: add hidden toolbar to examples
keithamus Mar 31, 2020
44edfbc
test: fixup example & test html
keithamus Apr 1, 2020
d91d2f0
fix: apply focus event listener only once
keithamus Apr 1, 2020
434e68c
style: move let binding closer to first use
keithamus Apr 1, 2020
b8777de
docs: add README note about data-md-button
keithamus Apr 1, 2020
dcfdbd7
docs: clarify focus management in readme
keithamus Apr 2, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
</head>
<body>
<div class="container py-4">
<p><button type="button">Add a Comment</button></p>
<markdown-toolbar for="textarea">
<md-bold class="btn btn-sm">bold</md-bold>
<md-header class="btn btn-sm">header</md-header>
Expand Down
42 changes: 41 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ class MarkdownButtonElement extends HTMLElement {

connectedCallback() {
if (!this.hasAttribute('tabindex')) {
this.setAttribute('tabindex', '0')
this.setAttribute('tabindex', '-1')
}

if (!this.hasAttribute('role')) {
this.setAttribute('role', 'button')
}
this.setAttribute('data-md-button', '')
}

click() {
Expand Down Expand Up @@ -221,11 +222,19 @@ class MarkdownToolbarElement extends HTMLElement {
}

connectedCallback() {
if (!this.hasAttribute('role')) {
this.setAttribute('role', 'toolbar')
}
const focusKeydownfn = focusKeydown.bind(null, this)
this.addEventListener('keydown', focusKeydownfn)
focusListeners.set(this, focusKeydownfn)
const fn = shortcut.bind(null, this)
if (this.field) {
this.field.addEventListener('keydown', fn)
shortcutListeners.set(this, fn)
}
const firstTabIndex = document.querySelector('[data-md-button]')
if (firstTabIndex) firstTabIndex.setAttribute('tabindex', '0')
}

disconnectedCallback() {
Expand All @@ -234,6 +243,10 @@ class MarkdownToolbarElement extends HTMLElement {
this.field.removeEventListener('keydown', fn)
shortcutListeners.delete(this)
}
const focusKeydownfn = focusListeners.get(this)
if (focusKeydownfn) {
this.removeEventListener('keydown', focusKeydownfn)
}
}

get field(): ?HTMLTextAreaElement {
Expand All @@ -244,6 +257,33 @@ class MarkdownToolbarElement extends HTMLElement {
}
}

const focusListeners = new WeakMap()

function focusKeydown(toolbar: MarkdownToolbarElement, event: KeyboardEvent) {
const key = event.key
if (key !== 'ArrowRight' && key !== 'ArrowLeft' && key !== 'Home' && key !== 'End') return
const target = event.target
if (!(target instanceof HTMLElement)) return
if (!target.hasAttribute('data-md-button')) return
if (target.closest('markdown-toolbar') !== toolbar) return

const buttons = []
for (const button of toolbar.querySelectorAll('[data-md-button]')) {
if (!(button instanceof MarkdownButtonElement)) continue
button.setAttribute('tabindex', '-1')
buttons.push(button)
}
let i = 0
if (key === 'ArrowLeft') i = buttons.indexOf(target) - 1
if (key === 'ArrowRight') i = buttons.indexOf(target) + 1
if (key === 'End') i = buttons.length - 1
if (i < 0) i = buttons.length - 1
if (i > buttons.length - 1) i = 0

buttons[i].setAttribute('tabindex', '0')
buttons[i].focus()
}

const shortcutListeners = new WeakMap()

function shortcut(toolbar: Element, event: KeyboardEvent) {
Expand Down
66 changes: 66 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,72 @@ describe('markdown-toolbar-element', function() {
document.body.innerHTML = ''
})

describe('focus management', function() {
function focusFirstButton() {
const button = document.querySelector('md-bold')
button.focus()
}

function pushKeyOnFocussedButton(key) {
const event = document.createEvent('Event')
event.initEvent('keydown', true, true)
event.key = key
document.activeElement.dispatchEvent(event)
}

function getElementsWithTabindex(index) {
return [...document.querySelectorAll(`markdown-toolbar [tabindex="${index}"]`)]
}

it('moves focus to next button when ArrowRight is pressed', function() {
focusFirstButton()
pushKeyOnFocussedButton('ArrowRight')
assert.equal(getElementsWithTabindex(-1).length, 13)
assert.deepEqual(getElementsWithTabindex(0), [document.querySelector('md-header')])
assert.deepEqual(getElementsWithTabindex(0), [document.activeElement])
pushKeyOnFocussedButton('ArrowRight')
assert.equal(getElementsWithTabindex(-1).length, 13)
assert.deepEqual(getElementsWithTabindex(0), [document.querySelector('md-header[level="1"]')])
assert.deepEqual(getElementsWithTabindex(0), [document.activeElement])
pushKeyOnFocussedButton('ArrowRight')
assert.equal(getElementsWithTabindex(-1).length, 13)
assert.deepEqual(getElementsWithTabindex(0), [document.querySelector('md-header[level="10"]')])
assert.deepEqual(getElementsWithTabindex(0), [document.activeElement])
})

it('cycles focus round to last element from first when ArrowLeft is pressed', function() {
focusFirstButton()
pushKeyOnFocussedButton('ArrowLeft')
assert.equal(getElementsWithTabindex(-1).length, 13)
assert.deepEqual(getElementsWithTabindex(0), [document.querySelector('md-ref')])
assert.deepEqual(getElementsWithTabindex(0), [document.activeElement])
pushKeyOnFocussedButton('ArrowLeft')
assert.equal(getElementsWithTabindex(-1).length, 13)
assert.deepEqual(getElementsWithTabindex(0), [document.querySelector('md-mention')])
assert.deepEqual(getElementsWithTabindex(0), [document.activeElement])
})

it('focussed first/last button when Home/End key is pressed', function() {
focusFirstButton()
pushKeyOnFocussedButton('End')
assert.equal(getElementsWithTabindex(-1).length, 13)
assert.deepEqual(getElementsWithTabindex(0), [document.querySelector('md-ref')])
assert.deepEqual(getElementsWithTabindex(0), [document.activeElement])
pushKeyOnFocussedButton('End')
assert.equal(getElementsWithTabindex(-1).length, 13)
assert.deepEqual(getElementsWithTabindex(0), [document.querySelector('md-ref')])
assert.deepEqual(getElementsWithTabindex(0), [document.activeElement])
pushKeyOnFocussedButton('Home')
assert.equal(getElementsWithTabindex(-1).length, 13)
assert.deepEqual(getElementsWithTabindex(0), [document.querySelector('md-bold')])
assert.deepEqual(getElementsWithTabindex(0), [document.activeElement])
pushKeyOnFocussedButton('Home')
assert.equal(getElementsWithTabindex(-1).length, 13)
assert.deepEqual(getElementsWithTabindex(0), [document.querySelector('md-bold')])
assert.deepEqual(getElementsWithTabindex(0), [document.activeElement])
})
})

describe('bold', function() {
it('bold selected text when you click the bold icon', function() {
setVisualValue('The |quick| brown fox jumps over the lazy dog')
Expand Down