Merge pull request #15163 from atom/fb-wb-modal-focus

Automatically manage focus for modal panels
This commit is contained in:
Bryant Ung 2017-08-31 17:09:02 -07:00 committed by GitHub
commit 96b3af143d
5 changed files with 84 additions and 5 deletions

View File

@ -32,6 +32,7 @@
"event-kit": "^2.3.0",
"find-parent-dir": "^0.3.0",
"first-mate": "7.0.7",
"focus-trap": "^2.3.0",
"fs-admin": "^0.1.5",
"fs-plus": "^3.0.1",
"fstream": "0.1.24",

View File

@ -17,6 +17,7 @@ describe('PanelContainerElement', () => {
this.model = model
return this
}
focus() {}
}
const TestPanelContainerItemElement = document.registerElement(
@ -159,5 +160,57 @@ describe('PanelContainerElement', () => {
expect(panel1.getElement()).toHaveClass('overlay')
expect(panel1.getElement()).toHaveClass('from-top')
})
describe("autoFocus", () => {
function createPanel() {
const panel = new Panel(
{
item: new TestPanelContainerItem(),
autoFocus: true,
visible: false
},
atom.views
)
container.addPanel(panel)
return panel
}
it("focuses the first tabbable item if available", () => {
const panel = createPanel()
const panelEl = panel.getElement()
const inputEl = document.createElement('input')
panelEl.appendChild(inputEl)
expect(document.activeElement).not.toBe(inputEl)
panel.show()
expect(document.activeElement).toBe(inputEl)
})
it("focuses the entire panel item when no tabbable item is available and the panel is focusable", () => {
const panel = createPanel()
const panelEl = panel.getElement()
spyOn(panelEl, 'focus')
panel.show()
expect(panelEl.focus).toHaveBeenCalled()
})
it("returns focus to the original activeElement", () => {
const panel = createPanel()
const previousActiveElement = document.activeElement
const panelEl = panel.getElement()
panelEl.appendChild(document.createElement('input'))
panel.show()
panel.hide()
waitsFor(() => document.activeElement === previousActiveElement)
runs(() => {
expect(document.activeElement).toBe(previousActiveElement)
})
})
})
})
})

View File

@ -1,5 +1,6 @@
'use strict'
const focusTrap = require('focus-trap')
const {CompositeDisposable} = require('event-kit')
class PanelContainerElement extends HTMLElement {
@ -52,6 +53,26 @@ class PanelContainerElement extends HTMLElement {
this.subscriptions.add(panel.onDidChangeVisible(visible => {
if (visible) { this.hideAllPanelsExcept(panel) }
}))
if (panel.autoFocus) {
const modalFocusTrap = focusTrap(panelElement, {
// focus-trap will attempt to give focus to the first tabbable element
// on activation. If there aren't any tabbable elements,
// give focus to the panel element itself
fallbackFocus: panelElement,
// closing is handled by core Atom commands and this already deactivates
// on visibility changes
escapeDeactivates: false
})
this.subscriptions.add(panel.onDidChangeVisible(visible => {
if (visible) {
modalFocusTrap.activate()
} else {
modalFocusTrap.deactivate()
}
}))
}
}
}

View File

@ -13,16 +13,15 @@ class Panel {
Section: Construction and Destruction
*/
constructor ({item, visible, priority, className}, viewRegistry) {
constructor ({item, autoFocus, visible, priority, className}, viewRegistry) {
this.destroyed = false
this.item = item
this.visible = visible
this.priority = priority
this.autoFocus = autoFocus == null ? false : autoFocus
this.visible = visible == null ? true : visible
this.priority = priority == null ? 100 : priority
this.className = className
this.viewRegistry = viewRegistry
this.emitter = new Emitter()
if (this.visible == null) this.visible = true
if (this.priority == null) this.priority = 100
}
// Public: Destroy and remove this panel from the UI.

View File

@ -1752,6 +1752,11 @@ module.exports = class Workspace extends Model {
// (default: true)
// * `priority` (optional) {Number} Determines stacking order. Lower priority items are
// forced closer to the edges of the window. (default: 100)
// * `autoFocus` (optional) {Boolean} true if you want modal focus managed for you by Atom.
// Atom will automatically focus your modal panel's first tabbable element when the modal
// opens and will restore the previously selected element when the modal closes. Atom will
// also automatically restrict user tab focus within your modal while it is open.
// (default: false)
//
// Returns a {Panel}
addModalPanel (options = {}) {