From b104485bb36e2088642c3611ebdd9c0e459001e0 Mon Sep 17 00:00:00 2001 From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Tue, 9 Jul 2024 21:09:54 +0900 Subject: [PATCH] =?UTF-8?q?Modal=E3=81=ABfocus=20trap=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/components/MkModal.vue | 13 ++++++- packages/frontend/src/scripts/focus-trap.ts | 37 ++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 packages/frontend/src/scripts/focus-trap.ts diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue index 264d8b6c9c..8d2462d5ee 100644 --- a/packages/frontend/src/components/MkModal.vue +++ b/packages/frontend/src/components/MkModal.vue @@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only })" :duration="transitionDuration" appear @afterLeave="emit('closed')" @enter="emit('opening')" @afterEnter="onOpened" > -
+
@@ -48,6 +48,7 @@ import { isTouchUsing } from '@/scripts/touch.js'; import { defaultStore } from '@/store.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { type Keymap } from '@/scripts/hotkey.js'; +import { focusTrap } from '@/scripts/focus-trap.js'; function getFixedContainer(el: Element | null): Element | null { if (el == null || el.tagName === 'BODY') return null; @@ -94,6 +95,7 @@ const maxHeight = ref(); const fixed = ref(false); const transformOrigin = ref('center'); const showing = ref(true); +const modalRootEl = shallowRef(); const content = shallowRef(); const zIndex = os.claimZIndex(props.zPriority); const useSendAnime = ref(false); @@ -132,6 +134,7 @@ const transitionDuration = computed((() => : 0 )); +let releaseFocusTrap: (() => void) | null = null; let contentClicking = false; function close(opts: { useSendAnimation?: boolean } = {}) { @@ -142,6 +145,7 @@ function close(opts: { useSendAnimation?: boolean } = {}) { // eslint-disable-next-line vue/no-mutating-props if (props.src) props.src.style.pointerEvents = 'auto'; showing.value = false; + releaseFocusTrap?.(); emit('close'); } @@ -283,6 +287,13 @@ const onOpened = () => { // NOTE: Chromatic テストの際に undefined になる場合がある if (content.value == null) return; + if (modalRootEl.value != null) { + const { release } = focusTrap(modalRootEl.value); + + releaseFocusTrap = release; + modalRootEl.value.focus(); + } + // モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する const el = content.value.children[0]; el.addEventListener('mousedown', ev => { diff --git a/packages/frontend/src/scripts/focus-trap.ts b/packages/frontend/src/scripts/focus-trap.ts new file mode 100644 index 0000000000..efd009af09 --- /dev/null +++ b/packages/frontend/src/scripts/focus-trap.ts @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { getHTMLElementOrNull } from '@/scripts/get-or-null.js'; + +function releaseFocusTrap(el: HTMLElement): void { + if (el.parentElement && el.parentElement !== document.body) { + el.parentElement.childNodes.forEach((siblingNode) => { + const siblingEl = getHTMLElementOrNull(siblingNode); + if (!siblingEl) return; + if (siblingEl !== el) { + siblingEl.inert = false; + } + }); + releaseFocusTrap(el.parentElement); + } +} + +export function focusTrap(el: HTMLElement): { release: () => void; } { + if (el.parentElement && el.parentElement !== document.body) { + el.parentElement.childNodes.forEach((siblingNode) => { + const siblingEl = getHTMLElementOrNull(siblingNode); + if (!siblingEl) return; + if (siblingEl !== el) { + siblingEl.inert = true; + } + }); + focusTrap(el.parentElement); + } + + return { + release: () => { + releaseFocusTrap(el); + }, + }; +}