Add a focus trap for overlays, and close them with Esc
This commit is contained in:
parent
246ef468de
commit
e64a553365
@ -33,22 +33,31 @@ export class Overlay {
|
|||||||
constructor(conductor, root) {
|
constructor(conductor, root) {
|
||||||
this.conductor = conductor;
|
this.conductor = conductor;
|
||||||
this.root = root;
|
this.root = root;
|
||||||
|
// Make the dialog itself focusable; this makes a lot of stuff easier, like ensuring that
|
||||||
|
// pressing Esc always has a viable target
|
||||||
|
this.root.tabIndex = 0;
|
||||||
|
|
||||||
// Don't propagate clicks on the root element, so they won't trigger a
|
// Don't propagate clicks on the root element, so they won't trigger a parent overlay's
|
||||||
// parent overlay's automatic dismissal
|
// automatic dismissal
|
||||||
this.root.addEventListener('click', ev => {
|
this.root.addEventListener('click', ev => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
});
|
});
|
||||||
// Block any window-level key handlers from firing when we type
|
// Don't propagate keys, either. This is only a partial solution (for when something within
|
||||||
|
// the dialog has focus), but open() adds another handler to block keys more aggressively
|
||||||
this.root.addEventListener('keydown', ev => {
|
this.root.addEventListener('keydown', ev => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
if (ev.key === 'Escape') {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
open() {
|
open() {
|
||||||
// FIXME ah, but keystrokes can still go to the game, including
|
if (this.root.isConnected) {
|
||||||
// spacebar to begin it if it was waiting. how do i completely disable
|
this.close();
|
||||||
// an entire chunk of the page?
|
}
|
||||||
|
|
||||||
if (this.conductor.player.state === 'playing') {
|
if (this.conductor.player.state === 'playing') {
|
||||||
this.conductor.player.set_state('paused');
|
this.conductor.player.set_state('paused');
|
||||||
}
|
}
|
||||||
@ -61,10 +70,76 @@ export class Overlay {
|
|||||||
this.close();
|
this.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Start with the overlay itself focused
|
||||||
|
this.root.focus();
|
||||||
|
|
||||||
|
// While this dialog is open, keys should not reach the rest of the document, and you should
|
||||||
|
// not be able to tab your way out of it. This is a rough implementation of that.
|
||||||
|
// Note that focusin bubbles, but focus doesn't. Also, focusin happens /just before/ an
|
||||||
|
// element receives focus, not afterwards, but that doesn't seem to affect this.
|
||||||
|
this.focusin_handler = ev => {
|
||||||
|
// If we're no longer visible at all, remove this event handler
|
||||||
|
if (! this.root.isConnected) {
|
||||||
|
this._remove_global_event_handlers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If we're not the topmost overlay, do nothing
|
||||||
|
if (this.root.parentNode.nextElementSibling)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// No problem if the focus is within the dialog, OR on the root <html> element
|
||||||
|
if (ev.target === document.documentElement || this.root.contains(ev.target)) {
|
||||||
|
this.last_focused = ev.target;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, focus is trying to escape! Put a stop to that.
|
||||||
|
// Focus was probably moved with tab or shift-tab. We should be the last element in the
|
||||||
|
// document, so tabbing off the end of us should go to browser UI. Shift-tabbing back
|
||||||
|
// beyond the start of a document usually goes to the root (and after that, browser UI
|
||||||
|
// again). Thus, there are only two common cases here: if the last valid focus was on
|
||||||
|
// the document root, they must be tabbing forwards, so focus our first element; if the
|
||||||
|
// last valid focus was within us, they must be tabbing backwards, so focus the root.
|
||||||
|
if (this.last_focused === document.documentElement) {
|
||||||
|
this.root.focus();
|
||||||
|
this.last_focused = this.root;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
document.documentElement.focus();
|
||||||
|
this.last_focused = document.documentElement;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('focusin', this.focusin_handler);
|
||||||
|
|
||||||
|
// Block any keypresses attempting to go to an element outside the dialog
|
||||||
|
this.keydown_handler = ev => {
|
||||||
|
// If we're no longer visible at all, remove this event handler
|
||||||
|
if (! this.root.isConnected) {
|
||||||
|
this._remove_global_event_handlers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If we're not the topmost overlay, do nothing
|
||||||
|
if (this.root.parentNode.nextElementSibling)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Note that if the target is the window itself, contains() will explode
|
||||||
|
if (! (ev.target instanceof Node && this.root.contains(ev.target))) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Use capture, which runs before any other event handler
|
||||||
|
window.addEventListener('keydown', this.keydown_handler, true);
|
||||||
|
|
||||||
return overlay;
|
return overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_remove_global_event_handlers() {
|
||||||
|
window.removeEventListener('focusin', this.focusin_handler);
|
||||||
|
window.removeEventListener('keydown', this.keydown_handler, true);
|
||||||
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
|
this._remove_global_event_handlers();
|
||||||
this.root.closest('.overlay').remove();
|
this.root.closest('.overlay').remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user