forked from cybrespace/pinafore
better a11y for modal: retain focus when closed
This commit is contained in:
parent
27f15d4030
commit
2e2c278dee
|
@ -0,0 +1,87 @@
|
|||
<div class="dialog-wrapper" ref:node>
|
||||
<div class="close-dialog-button-wrapper">
|
||||
<button class="close-dialog-button" aria-label="Close dialog" on:click="onCloseButtonClicked()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<style>
|
||||
:global(.modal-dialog) {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
background: #000;
|
||||
padding: 0;
|
||||
border: 3px solid var(--main-border);
|
||||
}
|
||||
:global(.modal-dialog-wrapper) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: calc(100vw - 40px);
|
||||
}
|
||||
.close-dialog-button-wrapper {
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
background: var(--nav-bg)
|
||||
}
|
||||
.close-dialog-button {
|
||||
margin: 0 0 2px;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
.close-dialog-button span {
|
||||
padding: 0 15px;
|
||||
font-size: 48px;
|
||||
color: var(--button-primary-text);
|
||||
}
|
||||
:global(dialog.modal-dialog::backdrop) {
|
||||
background: rgba(51, 51, 51, 0.8);
|
||||
}
|
||||
:global(dialog.modal-dialog + .backdrop) {
|
||||
background: rgba(51, 51, 51, 0.8);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
|
||||
import { importDialogPolyfill } from '../_utils/asyncModules'
|
||||
import { registerFocusRestoreDialog } from '../_utils/dialogs'
|
||||
|
||||
export default {
|
||||
oncreate() {
|
||||
// TODO: this hack is for Edge 16, which makes the modal too wide
|
||||
if (typeof setImmediate === 'function' && navigator.userAgent.match(/Edge/)) {
|
||||
this.getDialogElement().style.width = `${this.get('width')}px`
|
||||
}
|
||||
this.observe('shown', shown => {
|
||||
if (shown) {
|
||||
this.show()
|
||||
}
|
||||
})
|
||||
this.registration = this.register()
|
||||
},
|
||||
methods: {
|
||||
async register() {
|
||||
if (typeof HTMLDialogElement === 'undefined') {
|
||||
let dialogPolyfill = await importDialogPolyfill()
|
||||
dialogPolyfill.registerDialog(this.getDialogElement())
|
||||
}
|
||||
registerFocusRestoreDialog(this.getDialogElement())
|
||||
},
|
||||
async show() {
|
||||
await this.registration
|
||||
this.getDialogElement().showModal()
|
||||
},
|
||||
onCloseButtonClicked() {
|
||||
this.getDialogElement().close()
|
||||
document.body.removeChild(this.getDialogElement())
|
||||
},
|
||||
getDialogElement() {
|
||||
return this.refs.node.parentElement
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,9 +1,4 @@
|
|||
<div class="video-dialog-wrapper">
|
||||
<div class="close-video-button-wrapper">
|
||||
<button class="close-video-button" aria-label="Close dialog" on:click="onCloseButtonClicked()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<ModalDialog :shown>
|
||||
<video poster="{{poster}}"
|
||||
src="{{src}}"
|
||||
width="{{width}}"
|
||||
|
@ -11,75 +6,22 @@
|
|||
aria-label="Video: {{description || ''}}"
|
||||
controls
|
||||
/>
|
||||
</div>
|
||||
</ModalDialog>
|
||||
<style>
|
||||
:global(.video-dialog) {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
background: #000;
|
||||
padding: 0;
|
||||
border: 3px solid var(--main-border);
|
||||
}
|
||||
:global(.video-dialog-wrapper) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: calc(100vw - 40px);
|
||||
}
|
||||
:global(.video-dialog video) {
|
||||
:global(.modal-dialog video) {
|
||||
max-width: 100%;
|
||||
}
|
||||
.close-video-button-wrapper {
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
background: var(--nav-bg)
|
||||
}
|
||||
.close-video-button {
|
||||
margin: 0 0 2px;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
.close-video-button span {
|
||||
padding: 0 15px;
|
||||
font-size: 48px;
|
||||
color: var(--button-primary-text);
|
||||
}
|
||||
:global(dialog.video-dialog::backdrop) {
|
||||
background: rgba(51, 51, 51, 0.8);
|
||||
}
|
||||
:global(dialog.video-dialog + .backdrop) {
|
||||
background: rgba(51, 51, 51, 0.8);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
|
||||
import { importDialogPolyfill } from '../../_utils/asyncModules'
|
||||
import ModalDialog from '../ModalDialog.html'
|
||||
|
||||
export default {
|
||||
oncreate() {
|
||||
// TODO: this hack is for Edge 16, which makes the modal too wide
|
||||
if (typeof setImmediate === 'function' && navigator.userAgent.match(/Edge/)) {
|
||||
this.get('dialog').style.width = `${this.get('width')}px`
|
||||
}
|
||||
this.registration = this.register()
|
||||
components: {
|
||||
ModalDialog
|
||||
},
|
||||
methods: {
|
||||
async register() {
|
||||
if (typeof HTMLDialogElement === 'undefined') {
|
||||
let dialogPolyfill = await importDialogPolyfill()
|
||||
dialogPolyfill.registerDialog(this.get('dialog'))
|
||||
}
|
||||
},
|
||||
async showModal() {
|
||||
await this.registration
|
||||
this.get('dialog').showModal()
|
||||
},
|
||||
onCloseButtonClicked() {
|
||||
this.get('dialog').close()
|
||||
document.body.removeChild(this.get('dialog'))
|
||||
async show() {
|
||||
this.set({shown: true})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
// From https://gist.github.com/samthor/babe9fad4a65625b301ba482dad284d1
|
||||
// Via https://github.com/GoogleChrome/dialog-polyfill/issues/139
|
||||
|
||||
let registered = new WeakMap()
|
||||
|
||||
// store previous focused node centrally
|
||||
let previousFocus = null
|
||||
document.addEventListener('focusout', (e) => {
|
||||
previousFocus = e.target
|
||||
}, true)
|
||||
|
||||
/**
|
||||
* Updates the passed dialog to retain focus and restore it when the dialog is closed.
|
||||
* @param {!HTMLDialogElement} dialog to upgrade
|
||||
*/
|
||||
export function registerFocusRestoreDialog(dialog) {
|
||||
// replace showModal method directly, to save focus
|
||||
let realShowModal = dialog.showModal
|
||||
dialog.showModal = function () {
|
||||
let savedFocus = document.activeElement
|
||||
if (savedFocus === document || savedFocus === document.body) {
|
||||
// some browsers read activeElement as body
|
||||
savedFocus = previousFocus
|
||||
}
|
||||
registered.set(dialog, savedFocus)
|
||||
realShowModal.call(this)
|
||||
}
|
||||
|
||||
// on close, try to focus saved, if possible
|
||||
dialog.addEventListener('close', function () {
|
||||
if (dialog.hasAttribute('open')) {
|
||||
return // in native, this fires the frame later
|
||||
}
|
||||
let savedFocus = registered.get(dialog)
|
||||
if (document.contains(savedFocus)) {
|
||||
let wasFocus = document.activeElement
|
||||
savedFocus.focus()
|
||||
if (document.activeElement !== savedFocus) {
|
||||
wasFocus.focus() // restore focus, we couldn't focus saved
|
||||
}
|
||||
}
|
||||
savedFocus = null
|
||||
})
|
||||
}
|
||||
|
||||
export function createDialogElement(label) {
|
||||
if (!label) {
|
||||
throw new Error('the modal must have a label')
|
||||
}
|
||||
let dialogElement = document.createElement('dialog')
|
||||
dialogElement.classList.add('modal-dialog')
|
||||
dialogElement.setAttribute('aria-label', label)
|
||||
document.body.appendChild(dialogElement)
|
||||
return dialogElement
|
||||
}
|
|
@ -1,20 +1,16 @@
|
|||
import VideoDialog from '../_components/status/VideoDialog.html'
|
||||
import { createDialogElement } from './dialogs'
|
||||
|
||||
export function showVideoDialog(poster, src, width, height, description) {
|
||||
let dialog = document.createElement('dialog')
|
||||
dialog.classList.add('video-dialog')
|
||||
dialog.setAttribute('aria-label', 'Video dialog')
|
||||
document.body.appendChild(dialog)
|
||||
let videoDialog = new VideoDialog({
|
||||
target: dialog,
|
||||
target: createDialogElement('Video dialog'),
|
||||
data: {
|
||||
poster: poster,
|
||||
src: src,
|
||||
dialog: dialog,
|
||||
width: width,
|
||||
height: height,
|
||||
description: description
|
||||
}
|
||||
})
|
||||
videoDialog.showModal()
|
||||
videoDialog.show()
|
||||
}
|
Loading…
Reference in New Issue