fix(dialog): when dialog is hidden, don't scroll to top (#672)

* fix(dialog): when dialog is hidden, don't scroll to top

* update package-lock.json
This commit is contained in:
Nolan Lawson 2018-11-21 00:33:46 -08:00 committed by GitHub
parent 5fdba9366a
commit 689dae5d39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 428 additions and 13 deletions

5
package-lock.json generated
View File

@ -208,11 +208,6 @@
"resolved": "https://registry.npmjs.org/FileList/-/FileList-0.10.2.tgz",
"integrity": "sha1-YAOxqXFZNBZLZ8Q0rWqHQaHNFHo="
},
"a11y-dialog": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/a11y-dialog/-/a11y-dialog-4.0.1.tgz",
"integrity": "sha512-JsYMIaoZt7nZM1oDbxLBijyl09uZa91+UdPF+P9YadmmdtaVYx44v131UcqAhN48jNp8/BGV/80uMsBacPo2gg=="
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",

View File

@ -52,7 +52,6 @@
},
"dependencies": {
"@gamestdio/websocket": "^0.2.8",
"a11y-dialog": "^4.0.1",
"browserslist": "^4.3.4",
"cheerio": "^1.0.0-rc.2",
"child-process-promise": "^2.2.1",
@ -144,7 +143,8 @@
"Blob",
"Element",
"Image",
"NotificationEvent"
"NotificationEvent",
"NodeList"
],
"ignore": [
"dist",
@ -181,8 +181,7 @@
},
"greenkeeper": {
"ignore": [
"sapper",
"a11y-dialog"
"sapper"
]
},
"repository": {

View File

@ -28,7 +28,7 @@
</style>
<script>
import { store } from '../../_store/store'
import { autosize } from '../../_utils/autosize'
import { autosize } from '../../_thirdparty/autosize/autosize'
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
import debounce from 'lodash-es/debounce'
import { mark, stop } from '../../_utils/marks'

View File

@ -122,7 +122,7 @@
}
</style>
<script>
import A11yDialog from 'a11y-dialog'
import { A11yDialog } from '../../../_thirdparty/a11y-dialog/a11y-dialog'
import { classname } from '../../../_utils/classname'
import { on, emit } from '../../../_utils/eventBus'

View File

@ -0,0 +1,8 @@
The MIT License (MIT)
Copyright (c) 2017 Edenspiekermann
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,392 @@
// Forked from a11y-dialog 4.0.1, adding a small change to element.focus() to work
// around a Chrome bug with sticky positioning (https://github.com/nolanlawson/pinafore/issues/671)
// Original: https://unpkg.com/a11y-dialog@4.0.1/a11y-dialog.js
var FOCUSABLE_ELEMENTS = ['a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', 'iframe', 'object', 'embed', '[contenteditable]', '[tabindex]:not([tabindex^="-"])']
var TAB_KEY = 9
var ESCAPE_KEY = 27
var focusedBeforeDialog
/**
* Define the constructor to instantiate a dialog
*
* @constructor
* @param {Element} node
* @param {(NodeList | Element | string)} targets
*/
function A11yDialog (node, targets) {
// Prebind the functions that will be bound in addEventListener and
// removeEventListener to avoid losing references
this._show = this.show.bind(this)
this._hide = this.hide.bind(this)
this._maintainFocus = this._maintainFocus.bind(this)
this._bindKeypress = this._bindKeypress.bind(this)
// Keep a reference of the node on the instance
this.node = node
// Keep an object of listener types mapped to callback functions
this._listeners = {}
// Initialise everything needed for the dialog to work properly
this.create(targets)
}
/**
* Set up everything necessary for the dialog to be functioning
*
* @param {(NodeList | Element | string)} targets
* @return {this}
*/
A11yDialog.prototype.create = function (targets) {
// Keep a collection of nodes to disable/enable when toggling the dialog
this._targets = this._targets || collect(targets) || getSiblings(this.node)
// Make sure the dialog element is disabled on load, and that the `shown`
// property is synced with its value
this.node.setAttribute('aria-hidden', true)
this.shown = false
// Keep a collection of dialog openers, each of which will be bound a click
// event listener to open the dialog
this._openers = $$('[data-a11y-dialog-show="' + this.node.id + '"]')
this._openers.forEach(function (opener) {
opener.addEventListener('click', this._show)
}.bind(this))
// Keep a collection of dialog closers, each of which will be bound a click
// event listener to close the dialog
this._closers = $$('[data-a11y-dialog-hide]', this.node)
.concat($$('[data-a11y-dialog-hide="' + this.node.id + '"]'))
this._closers.forEach(function (closer) {
closer.addEventListener('click', this._hide)
}.bind(this))
// Execute all callbacks registered for the `create` event
this._fire('create')
return this
}
/**
* Show the dialog element, disable all the targets (siblings), trap the
* current focus within it, listen for some specific key presses and fire all
* registered callbacks for `show` event
*
* @param {Event} event
* @return {this}
*/
A11yDialog.prototype.show = function (event) {
// If the dialog is already open, abort
if (this.shown) {
return this
}
this.shown = true
this.node.removeAttribute('aria-hidden')
// Iterate over the targets to disable them by setting their `aria-hidden`
// attribute to `true`; in case they already have this attribute, keep a
// reference of their original value to be able to restore it later
this._targets.forEach(function (target) {
var original = target.getAttribute('aria-hidden')
if (original) {
target.setAttribute('data-a11y-dialog-original', original)
}
target.setAttribute('aria-hidden', 'true')
})
// Keep a reference to the currently focused element to be able to restore
// it later, then set the focus to the first focusable child of the dialog
// element
focusedBeforeDialog = document.activeElement
setFocusToFirstItem(this.node)
// Bind a focus event listener to the body element to make sure the focus
// stays trapped inside the dialog while open, and start listening for some
// specific key presses (TAB and ESC)
document.body.addEventListener('focus', this._maintainFocus, true)
document.addEventListener('keydown', this._bindKeypress)
// Execute all callbacks registered for the `show` event
this._fire('show', event)
return this
}
/**
* Hide the dialog element, enable all the targets (siblings), restore the
* focus to the previously active element, stop listening for some specific
* key presses and fire all registered callbacks for `hide` event
*
* @param {Event} event
* @return {this}
*/
A11yDialog.prototype.hide = function (event) {
// If the dialog is already closed, abort
if (!this.shown) {
return this
}
this.shown = false
this.node.setAttribute('aria-hidden', 'true')
// Iterate over the targets to enable them by remove their `aria-hidden`
// attribute or resetting them to their initial value
this._targets.forEach(function (target) {
var original = target.getAttribute('data-a11y-dialog-original')
if (original) {
target.setAttribute('aria-hidden', original)
target.removeAttribute('data-a11y-dialog-original')
} else {
target.removeAttribute('aria-hidden')
}
})
// If their was a focused element before the dialog was opened, restore the
// focus back to it
if (focusedBeforeDialog) {
// This double rAF is to work around a bug in Chrome when focusing sticky-positioned
// elements. See https://github.com/nolanlawson/pinafore/issues/671
requestAnimationFrame(() => requestAnimationFrame(() => focusedBeforeDialog.focus()))
}
// Remove the focus event listener to the body element and stop listening
// for specific key presses
document.body.removeEventListener('focus', this._maintainFocus, true)
document.removeEventListener('keydown', this._bindKeypress)
// Execute all callbacks registered for the `hide` event
this._fire('hide', event)
return this
}
/**
* Destroy the current instance (after making sure the dialog has been hidden)
* and remove all associated listeners from dialog openers and closers
*
* @return {this}
*/
A11yDialog.prototype.destroy = function () {
// Hide the dialog to avoid destroying an open instance
this.hide()
// Remove the click event listener from all dialog openers
this._openers.forEach(function (opener) {
opener.removeEventListener('click', this._show)
}.bind(this))
// Remove the click event listener from all dialog closers
this._closers.forEach(function (closer) {
closer.removeEventListener('click', this._hide)
}.bind(this))
// Execute all callbacks registered for the `destroy` event
this._fire('destroy')
// Keep an object of listener types mapped to callback functions
this._listeners = {}
return this
}
/**
* Register a new callback for the given event type
*
* @param {string} type
* @param {Function} handler
*/
A11yDialog.prototype.on = function (type, handler) {
if (typeof this._listeners[type] === 'undefined') {
this._listeners[type] = []
}
this._listeners[type].push(handler)
return this
}
/**
* Unregister an existing callback for the given event type
*
* @param {string} type
* @param {Function} handler
*/
A11yDialog.prototype.off = function (type, handler) {
var index = this._listeners[type].indexOf(handler)
if (index > -1) {
this._listeners[type].splice(index, 1)
}
return this
}
/**
* Iterate over all registered handlers for given type and call them all with
* the dialog element as first argument, event as second argument (if any).
*
* @access private
* @param {string} type
* @param {Event} event
*/
A11yDialog.prototype._fire = function (type, event) {
var listeners = this._listeners[type] || []
listeners.forEach(function (listener) {
listener(this.node, event)
}.bind(this))
}
/**
* Private event handler used when listening to some specific key presses
* (namely ESCAPE and TAB)
*
* @access private
* @param {Event} event
*/
A11yDialog.prototype._bindKeypress = function (event) {
// If the dialog is shown and the ESCAPE key is being pressed, prevent any
// further effects from the ESCAPE key and hide the dialog
if (this.shown && event.which === ESCAPE_KEY) {
event.preventDefault()
this.hide()
}
// If the dialog is shown and the TAB key is being pressed, make sure the
// focus stays trapped within the dialog element
if (this.shown && event.which === TAB_KEY) {
trapTabKey(this.node, event)
}
}
/**
* Private event handler used when making sure the focus stays within the
* currently open dialog
*
* @access private
* @param {Event} event
*/
A11yDialog.prototype._maintainFocus = function (event) {
// If the dialog is shown and the focus is not within the dialog element,
// move it back to its first focusable child
if (this.shown && !this.node.contains(event.target)) {
setFocusToFirstItem(this.node)
}
}
/**
* Convert a NodeList into an array
*
* @param {NodeList} collection
* @return {Array<Element>}
*/
function toArray (collection) {
return Array.prototype.slice.call(collection)
}
/**
* Query the DOM for nodes matching the given selector, scoped to context (or
* the whole document)
*
* @param {String} selector
* @param {Element} [context = document]
* @return {Array<Element>}
*/
function $$ (selector, context) {
return toArray((context || document).querySelectorAll(selector))
}
/**
* Return an array of Element based on given argument (NodeList, Element or
* string representing a selector)
*
* @param {(NodeList | Element | string)} target
* @return {Array<Element>}
*/
function collect (target) {
if (NodeList.prototype.isPrototypeOf(target)) {
return toArray(target)
}
if (Element.prototype.isPrototypeOf(target)) {
return [target]
}
if (typeof target === 'string') {
return $$(target)
}
}
/**
* Set the focus to the first focusable child of the given element
*
* @param {Element} node
*/
function setFocusToFirstItem (node) {
var focusableChildren = getFocusableChildren(node)
if (focusableChildren.length) {
focusableChildren[0].focus()
}
}
/**
* Get the focusable children of the given element
*
* @param {Element} node
* @return {Array<Element>}
*/
function getFocusableChildren (node) {
return $$(FOCUSABLE_ELEMENTS.join(','), node).filter(function (child) {
return !!(child.offsetWidth || child.offsetHeight || child.getClientRects().length)
})
}
/**
* Trap the focus inside the given element
*
* @param {Element} node
* @param {Event} event
*/
function trapTabKey (node, event) {
var focusableChildren = getFocusableChildren(node)
var focusedItemIndex = focusableChildren.indexOf(document.activeElement)
// If the SHIFT key is being pressed while tabbing (moving backwards) and
// the currently focused item is the first one, move the focus to the last
// focusable item from the dialog element
if (event.shiftKey && focusedItemIndex === 0) {
focusableChildren[focusableChildren.length - 1].focus()
event.preventDefault()
// If the SHIFT key is not being pressed (moving forwards) and the currently
// focused item is the last one, move the focus to the first focusable item
// from the dialog element
} else if (!event.shiftKey && focusedItemIndex === focusableChildren.length - 1) {
focusableChildren[0].focus()
event.preventDefault()
}
}
/**
* Retrieve siblings from given element
*
* @param {Element} node
* @return {Array<Element>}
*/
function getSiblings (node) {
var nodes = toArray(node.parentNode.childNodes)
var siblings = nodes.filter(function (node) {
return node.nodeType === 1
})
siblings.splice(siblings.indexOf(node), 1)
return siblings
}
export { A11yDialog }

21
routes/_thirdparty/autosize/LICENSE.md vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Jack Moore
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -3,10 +3,10 @@
// remove parent overflow checks, make page resizes more performant,
// add deferredUpdate, and add perf marks.
import { mark, stop } from './marks'
import { mark, stop } from '../../_utils/marks'
import debounce from 'lodash-es/debounce'
import throttle from 'lodash-es/throttle'
import { getScrollContainer } from './scrollContainer'
import { getScrollContainer } from '../../_utils/scrollContainer'
const map = new Map()
let createEvent = (name) => new Event(name, { bubbles: true })