fix(frontend): フォーカスの挙動を修正 (#14158)
* fix(frontend): 直前のパターンを記録するように * fix(frontend): フォーカス/タブ移動に関する挙動を調整 (#226) Cherry-pick commit e8c030673326871edf3623cf2b8675d68f9e1b13 Co-authored-by: taiyme <53635909+taiyme@users.noreply.github.com> * focusのデザイン修正 * move scripts * Modalにfocus trapを追加 * 記録するホットキーはレートリミット式にする * escキーのハンドリングをMkModalに統一 * fix * enterで子メニューを開けるように * lint * fix focus trap * improve switch accessibility * 一部のmodalのフォーカストラップが外れない問題を修正 * fix * fix * Revert "記録するホットキーはレートリミット式にする" This reverts commit40a7509286
. * Revert "fix(frontend): 直前のパターンを記録するように" This reverts commit5372b25940
. * Revert "Revert "fix(frontend): 直前のパターンを記録するように"" This reverts commita9bb52e799
. * Revert "Revert "記録するホットキーはレートリミット式にする"" This reverts commitbdac34273e
. * 試験的にCypressでのFocustrapを無効化 * fix * fix focus-trap * Update Changelog * ✌️ * fix focustrap invocation logic * スクロールがsticky headerを考慮するように * 🎨 * スタイルの微調整 * 🎨 * remove deprecated key aliases * focusElementが足りなかったので修正 * preview系にfocus時スタイルが足りなかったので修正 * `returnFocusElement` -> `returnFocusTo` * lint * Update packages/frontend/src/components/MkModalWindow.vue * Apply suggestions from code review Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com> * keydownイベントをまとめる * use correct pesudo-element selector * fix * rename --------- Co-authored-by: taiyme <53635909+taiyme@users.noreply.github.com> Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
ref="rootEl"
|
||||
v-hotkey="keymap"
|
||||
:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
|
||||
:tabindex="!isDeleted ? '-1' : undefined"
|
||||
:tabindex="isDeleted ? '-1' : '0'"
|
||||
>
|
||||
<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
|
||||
<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
|
||||
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
</I18n>
|
||||
<div :class="$style.renoteInfo">
|
||||
<button ref="renoteTime" :class="$style.renoteTime" class="_button" @click="showRenoteMenu()">
|
||||
<button ref="renoteTime" :class="$style.renoteTime" class="_button" @mousedown.prevent="showRenoteMenu()">
|
||||
<i class="ti ti-dots" :class="$style.renoteMenu"></i>
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</button>
|
||||
@@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="appearNote.files && appearNote.files.length > 0">
|
||||
<MkMediaList :mediaList="appearNote.files"/>
|
||||
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
|
||||
</div>
|
||||
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
|
||||
<div v-if="isEnabledUrlPreview">
|
||||
@@ -110,7 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
ref="renoteButton"
|
||||
:class="$style.footerButton"
|
||||
class="_button"
|
||||
@mousedown="renote()"
|
||||
@mousedown.prevent="renote()"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
||||
@@ -125,10 +125,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<i v-else class="ti ti-plus"></i>
|
||||
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
||||
</button>
|
||||
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
|
||||
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()">
|
||||
<i class="ti ti-paperclip"></i>
|
||||
</button>
|
||||
<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="showMenu()">
|
||||
<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown.prevent="showMenu()">
|
||||
<i class="ti ti-dots"></i>
|
||||
</button>
|
||||
</footer>
|
||||
@@ -175,7 +175,6 @@ import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||
import { checkWordMute } from '@/scripts/check-word-mute.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import number from '@/filters/number.js';
|
||||
@@ -199,6 +198,7 @@ import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||
import { shouldCollapsed } from '@/scripts/collapsed.js';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
import { type Keymap } from '@/scripts/hotkey.js';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
@@ -257,6 +257,7 @@ const renoteTime = shallowRef<HTMLElement>();
|
||||
const reactButton = shallowRef<HTMLElement>();
|
||||
const clipButton = shallowRef<HTMLElement>();
|
||||
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
||||
const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
|
||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||
const showContent = ref(false);
|
||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
||||
@@ -318,7 +319,7 @@ const keymap = {
|
||||
},
|
||||
'o': () => {
|
||||
if (renoteCollapsed.value) return;
|
||||
showMenu();
|
||||
galleryEl.value?.openGallery();
|
||||
},
|
||||
'v|enter': () => {
|
||||
if (renoteCollapsed.value) {
|
||||
@@ -419,7 +420,7 @@ function renote(viaKeyboard = false) {
|
||||
});
|
||||
}
|
||||
|
||||
function reply(viaKeyboard = false): void {
|
||||
function reply(): void {
|
||||
pleaseLogin();
|
||||
if (props.mock) {
|
||||
return;
|
||||
@@ -427,13 +428,12 @@ function reply(viaKeyboard = false): void {
|
||||
os.post({
|
||||
reply: appearNote.value,
|
||||
channel: appearNote.value.channel,
|
||||
animation: !viaKeyboard,
|
||||
}).then(() => {
|
||||
focus();
|
||||
});
|
||||
}
|
||||
|
||||
function react(viaKeyboard = false): void {
|
||||
function react(): void {
|
||||
pleaseLogin();
|
||||
showMovedDialog();
|
||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||
@@ -528,18 +528,16 @@ function onContextmenu(ev: MouseEvent): void {
|
||||
}
|
||||
}
|
||||
|
||||
function showMenu(viaKeyboard = false): void {
|
||||
function showMenu(): void {
|
||||
if (props.mock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
|
||||
os.popupMenu(menu, menuButton.value, {
|
||||
viaKeyboard,
|
||||
}).then(focus).finally(cleanup);
|
||||
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
|
||||
}
|
||||
|
||||
async function clip() {
|
||||
async function clip(): Promise<void> {
|
||||
if (props.mock) {
|
||||
return;
|
||||
}
|
||||
@@ -547,7 +545,7 @@ async function clip() {
|
||||
os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
|
||||
}
|
||||
|
||||
function showRenoteMenu(viaKeyboard = false): void {
|
||||
function showRenoteMenu(): void {
|
||||
if (props.mock) {
|
||||
return;
|
||||
}
|
||||
@@ -572,18 +570,14 @@ function showRenoteMenu(viaKeyboard = false): void {
|
||||
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
||||
{ type: 'divider' },
|
||||
getUnrenote(),
|
||||
], renoteTime.value, {
|
||||
viaKeyboard: viaKeyboard,
|
||||
});
|
||||
], renoteTime.value);
|
||||
} else {
|
||||
os.popupMenu([
|
||||
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
||||
{ type: 'divider' },
|
||||
getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
|
||||
($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined,
|
||||
], renoteTime.value, {
|
||||
viaKeyboard: viaKeyboard,
|
||||
});
|
||||
], renoteTime.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,11 +590,11 @@ function blur() {
|
||||
}
|
||||
|
||||
function focusBefore() {
|
||||
focusPrev(rootEl.value ?? null);
|
||||
focusPrev(rootEl.value);
|
||||
}
|
||||
|
||||
function focusAfter() {
|
||||
focusNext(rootEl.value ?? null);
|
||||
focusNext(rootEl.value);
|
||||
}
|
||||
|
||||
function readPromo() {
|
||||
@@ -638,7 +632,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
|
||||
&:after {
|
||||
&::after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
@@ -651,7 +645,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
||||
margin: auto;
|
||||
width: calc(100% - 8px);
|
||||
height: calc(100% - 8px);
|
||||
border: dashed 1px var(--focus);
|
||||
border: dashed 2px var(--focus);
|
||||
border-radius: var(--radius);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
Reference in New Issue
Block a user