Merge pull request #9380 from 370417/modal-a11y
Improve keyboard navigation of modalspull/9419/head
commit
a30b3e358f
|
@ -1,4 +1,5 @@
|
|||
import { h, VNode } from 'snabbdom';
|
||||
import { focusFirstChild } from 'common/modal';
|
||||
import { MaybeVNodes } from './interfaces';
|
||||
import { bind, onInsert } from './util';
|
||||
|
||||
|
@ -21,14 +22,23 @@ export function modal(d: Modal): VNode {
|
|||
'div#modal-wrap.study__modal.' + d.class,
|
||||
{
|
||||
hook: onInsert(el => {
|
||||
focusFirstChild($(el));
|
||||
el.addEventListener('mousedown', e => e.stopPropagation());
|
||||
d.onInsert && d.onInsert(el);
|
||||
}),
|
||||
},
|
||||
[
|
||||
h('span.close', {
|
||||
attrs: { 'data-icon': '' },
|
||||
hook: bind('click', d.onClose),
|
||||
attrs: {
|
||||
'data-icon': '',
|
||||
role: 'button',
|
||||
'aria-label': 'Close',
|
||||
tabindex: '0',
|
||||
},
|
||||
hook: onInsert(el => {
|
||||
el.addEventListener('click', d.onClose);
|
||||
el.addEventListener('keydown', e => (e.code === 'Enter' || e.code === 'Space' ? d.onClose() : true));
|
||||
}),
|
||||
}),
|
||||
h('div', d.content),
|
||||
]
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
export default function modal(content: Cash, cls?: string, onClose?: () => void) {
|
||||
modal.close();
|
||||
const $wrap: any = $('<div id="modal-wrap"><span class="close" data-icon=""></span></div>');
|
||||
const $wrap = $(
|
||||
'<div id="modal-wrap"><span class="close" role="button" aria-label="Close" data-icon="" tabindex="0"></span></div>'
|
||||
);
|
||||
const $overlay = $(`<div id="modal-overlay" class="${cls}">`).on('click', modal.close);
|
||||
$('<a href="#"></a>').appendTo($overlay); // guard against focus escaping to window chrome
|
||||
$wrap.appendTo($overlay);
|
||||
$('<a href="#"></a>').appendTo($overlay); // guard against focus escaping to window chrome
|
||||
content.clone().removeClass('none').appendTo($wrap);
|
||||
modal.onClose = onClose;
|
||||
$wrap.find('.close').on('click', modal.close);
|
||||
$wrap
|
||||
.find('.close')
|
||||
.on('click', modal.close)
|
||||
.on('keydown', (e: KeyboardEvent) => (e.code === 'Space' || e.code === 'Enter' ? modal.close() : true));
|
||||
$wrap.on('click', (e: Event) => e.stopPropagation());
|
||||
$('body').addClass('overlayed').prepend($overlay);
|
||||
focusFirstChild($wrap);
|
||||
return $wrap;
|
||||
}
|
||||
modal.close = () => {
|
||||
|
@ -19,3 +27,23 @@ modal.close = () => {
|
|||
delete modal.onClose;
|
||||
};
|
||||
modal.onClose = undefined as (() => void) | undefined;
|
||||
|
||||
const focusableSelectors =
|
||||
'button:not(:disabled), [href], input:not(:disabled):not([type="hidden"]), select:not(:disabled), textarea:not(:disabled), [tabindex="0"]';
|
||||
|
||||
export function trapFocus(event: FocusEvent) {
|
||||
const wrap: HTMLElement | undefined = $('#modal-wrap').get(0);
|
||||
if (!wrap) return;
|
||||
const position = wrap.compareDocumentPosition(event.target as HTMLElement);
|
||||
if (position & Node.DOCUMENT_POSITION_CONTAINED_BY) return;
|
||||
const focusableChildren = $(wrap).find(focusableSelectors);
|
||||
const index = position & Node.DOCUMENT_POSITION_FOLLOWING ? 0 : focusableChildren.length - 1;
|
||||
focusableChildren.get(index)?.focus();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
export function focusFirstChild(parent: Cash) {
|
||||
const children = parent.find(focusableSelectors);
|
||||
// prefer child 1 over child 0 because child 0 should be a close button
|
||||
(children.get(1) ?? children.get(0))?.focus();
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import watchers from './component/watchers';
|
|||
import { reload } from './component/reload';
|
||||
import { requestIdleCallback } from './component/functions';
|
||||
import { userComplete } from './component/assets';
|
||||
import { trapFocus } from 'common/modal';
|
||||
|
||||
exportLichessGlobals();
|
||||
lichess.info = info;
|
||||
|
@ -115,6 +116,8 @@ lichess.load.then(() => {
|
|||
return false;
|
||||
});
|
||||
|
||||
$('body').on('focusin', trapFocus);
|
||||
|
||||
window.Mousetrap.bind('esc', () => {
|
||||
const $oc = $('#modal-wrap .close');
|
||||
if ($oc.length) $oc.trigger('click');
|
||||
|
|
|
@ -2,6 +2,7 @@ import TournamentController from '../ctrl';
|
|||
import { bind, onInsert, playerName } from './util';
|
||||
import { h, VNode } from 'snabbdom';
|
||||
import { TeamBattle, RankedTeam, MaybeVNode } from '../interfaces';
|
||||
import { focusFirstChild } from 'common/modal';
|
||||
|
||||
export function joinWithTeamSelector(ctrl: TournamentController) {
|
||||
const onClose = () => {
|
||||
|
@ -19,13 +20,22 @@ export function joinWithTeamSelector(ctrl: TournamentController) {
|
|||
'div#modal-wrap.team-battle__choice',
|
||||
{
|
||||
hook: onInsert(el => {
|
||||
focusFirstChild($(el));
|
||||
el.addEventListener('click', e => e.stopPropagation());
|
||||
}),
|
||||
},
|
||||
[
|
||||
h('span.close', {
|
||||
attrs: { 'data-icon': '' },
|
||||
hook: bind('click', onClose),
|
||||
attrs: {
|
||||
'data-icon': '',
|
||||
role: 'button',
|
||||
'aria-label': 'Close',
|
||||
tabindex: '0',
|
||||
},
|
||||
hook: onInsert(el => {
|
||||
el.addEventListener('click', onClose);
|
||||
el.addEventListener('keydown', e => (e.code === 'Enter' || e.code === 'Space' ? onClose() : true));
|
||||
}),
|
||||
}),
|
||||
h('div.team-picker', [
|
||||
h('h2', 'Pick your team'),
|
||||
|
|
Loading…
Reference in New Issue