Proper use of focus trap

This commit is contained in:
Freeplay 2023-06-20 14:13:05 -04:00
parent be6a640edb
commit c05f151b1a
4 changed files with 63 additions and 84 deletions

View file

@ -1,10 +1,12 @@
<template> <template>
<FocusTrap <FocusTrap
:active="false"
ref="focusTrap" ref="focusTrap"
v-model:active="isActive"
:return-focus-on-deactivate="!noReturnFocus" :return-focus-on-deactivate="!noReturnFocus"
:initial-focus="() => itemsEl.children[0]"
@deactivate="emit('close')"
> >
<div tabindex="-1"> <div>
<div <div
ref="itemsEl" ref="itemsEl"
class="rrevdjwt _popup _shadow" class="rrevdjwt _popup _shadow"
@ -14,6 +16,7 @@
maxHeight: maxHeight ? maxHeight + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '',
}" }"
@contextmenu.self="(e) => e.preventDefault()" @contextmenu.self="(e) => e.preventDefault()"
tabindex="-1"
> >
<template v-for="(item, i) in items2"> <template v-for="(item, i) in items2">
<div v-if="item === null" class="divider"></div> <div v-if="item === null" class="divider"></div>
@ -173,6 +176,7 @@
:root-element="itemsEl" :root-element="itemsEl"
showing showing
@actioned="childActioned" @actioned="childActioned"
@closed="closeChild"
/> />
</div> </div>
</div> </div>
@ -303,23 +307,7 @@ function close(actioned = false) {
emit("close", actioned); emit("close", actioned);
} }
function focusUp() {
focusPrev(document.activeElement);
}
function focusDown() {
focusNext(document.activeElement);
}
onMounted(() => { onMounted(() => {
focusTrap.value.activate();
if (props.viaKeyboard) {
nextTick(() => {
focusNext(itemsEl.children[0], true, false);
});
}
document.addEventListener("mousedown", onGlobalMousedown, { document.addEventListener("mousedown", onGlobalMousedown, {
passive: true, passive: true,
}); });

View file

@ -14,17 +14,17 @@
:duration="transitionDuration" :duration="transitionDuration"
appear appear
@after-leave="emit('closed')" @after-leave="emit('closed')"
@keyup.esc="emit('click')"
@enter="emit('opening')" @enter="emit('opening')"
@after-enter="onOpened" @after-enter="onOpened"
> >
<FocusTrap <FocusTrap
v-model:active="isActive" v-model:active="isActive"
:initial-focus="() => $refs.content"
:return-focus-on-deactivate="!noReturnFocus" :return-focus-on-deactivate="!noReturnFocus"
@deactivate="close"
> >
<div <div
v-show="manualShowing != null ? manualShowing : showing" v-show="manualShowing != null ? manualShowing : showing"
v-hotkey.global="keymap"
:class="[ :class="[
$style.root, $style.root,
{ {
@ -44,7 +44,6 @@
'--transformOrigin': transformOrigin, '--transformOrigin': transformOrigin,
}" }"
tabindex="-1" tabindex="-1"
v-focus
> >
<div <div
class="_modalBg data-cy-bg" class="_modalBg data-cy-bg"
@ -180,7 +179,7 @@ let transitionDuration = $computed(() =>
let contentClicking = false; let contentClicking = false;
const focusedElement = document.activeElement; // const focusedElement = document.activeElement;
function close(ev, opts: { useSendAnimation?: boolean } = {}) { function close(ev, opts: { useSendAnimation?: boolean } = {}) {
// removeEventListener("popstate", close); // removeEventListener("popstate", close);
// if (props.preferType == "dialog") { // if (props.preferType == "dialog") {
@ -194,16 +193,16 @@ function close(ev, opts: { useSendAnimation?: boolean } = {}) {
if (props.src) props.src.style.pointerEvents = "auto"; if (props.src) props.src.style.pointerEvents = "auto";
showing = false; showing = false;
emit("close"); emit("close");
if (!props.noReturnFocus) { // if (!props.noReturnFocus) {
focusedElement.focus(); // focusedElement.focus();
} // }
} }
function onBgClick() { function onBgClick() {
if (contentClicking) return; if (contentClicking) return;
if (!props.noReturnFocus) { // if (!props.noReturnFocus) {
focusedElement.focus(); // focusedElement.focus();
} // }
emit("click"); emit("click");
} }
@ -211,10 +210,6 @@ if (type === "drawer") {
maxHeight = window.innerHeight / 1.5; maxHeight = window.innerHeight / 1.5;
} }
const keymap = {
esc: () => emit("esc"),
};
const MARGIN = 16; const MARGIN = 16;
const align = () => { const align = () => {

View file

@ -6,58 +6,55 @@
@keyup.esc="$emit('close')" @keyup.esc="$emit('close')"
@closed="$emit('closed')" @closed="$emit('closed')"
> >
<FocusTrap v-model:active="isActive"> <div
<div ref="rootEl"
ref="rootEl" class="ebkgoccj"
class="ebkgoccj" :style="{
:style="{ width: `${width}px`,
width: `${width}px`, height: scroll
height: scroll ? height
? height ? `${height}px`
? `${height}px` : null
: null : height
: height ? `min(${height}px, 100%)`
? `min(${height}px, 100%)` : '100%',
: '100%', }"
}" tabindex="-1"
@keydown="onKeydown" >
tabindex="-1" <div ref="headerEl" class="header">
> <button
<div ref="headerEl" class="header"> v-if="withOkButton"
<button :aria-label="i18n.t('close')"
v-if="withOkButton" class="_button"
:aria-label="i18n.t('close')" @click="$emit('close')"
class="_button" >
@click="$emit('close')" <i class="ph-x ph-bold ph-lg"></i>
> </button>
<i class="ph-x ph-bold ph-lg"></i> <span class="title">
</button> <slot name="header"></slot>
<span class="title"> </span>
<slot name="header"></slot> <button
</span> v-if="!withOkButton"
<button :aria-label="i18n.t('close')"
v-if="!withOkButton" class="_button"
:aria-label="i18n.t('close')" @click="$emit('close')"
class="_button" >
@click="$emit('close')" <i class="ph-x ph-bold ph-lg"></i>
> </button>
<i class="ph-x ph-bold ph-lg"></i> <button
</button> v-if="withOkButton"
<button :aria-label="i18n.t('ok')"
v-if="withOkButton" class="_button"
:aria-label="i18n.t('ok')" :disabled="okButtonDisabled"
class="_button" @click="$emit('ok')"
:disabled="okButtonDisabled" >
@click="$emit('ok')" <i class="ph-check ph-bold ph-lg"></i>
> </button>
<i class="ph-check ph-bold ph-lg"></i>
</button>
</div>
<div class="body">
<slot></slot>
</div>
</div> </div>
</FocusTrap> <div class="body">
<slot></slot>
</div>
</div>
</MkModal> </MkModal>
</template> </template>

View file

@ -8,7 +8,6 @@
@click="modal.close()" @click="modal.close()"
@closed="emit('closed')" @closed="emit('closed')"
tabindex="-1" tabindex="-1"
v-focus
> >
<MkMenu <MkMenu
:items="items" :items="items"