From 08686da50e8167c1309685a65cd27bc3f8af27c9 Mon Sep 17 00:00:00 2001 From: Free <freeplay@duck.com> Date: Sat, 29 Apr 2023 22:30:14 +0000 Subject: [PATCH] keyboard accessibility (#9725) Reviewed-on: https://codeberg.org/calckey/calckey/pulls/9725 Co-authored-by: Free <freeplay@duck.com> Co-committed-by: Free <freeplay@duck.com> --- package.json | 2 + packages/client/src/components/MkButton.vue | 3 +- packages/client/src/components/MkCwButton.vue | 18 +- .../src/components/MkDriveFileThumbnail.vue | 7 +- .../client/src/components/MkEmojiPicker.vue | 278 ++++++------- .../client/src/components/MkLaunchPad.vue | 2 +- .../client/src/components/MkMediaImage.vue | 4 + .../client/src/components/MkMenu.child.vue | 21 +- packages/client/src/components/MkMenu.vue | 378 +++++++++--------- packages/client/src/components/MkModal.vue | 86 ++-- .../src/components/MkModalPageWindow.vue | 1 + .../client/src/components/MkModalWindow.vue | 95 ++--- packages/client/src/components/MkNote.vue | 8 +- .../client/src/components/MkNotePreview.vue | 2 +- packages/client/src/components/MkNoteSub.vue | 4 +- .../client/src/components/MkPopupMenu.vue | 2 + .../src/components/MkPostFormAttaches.vue | 1 - .../src/components/MkSubNoteContent.vue | 27 +- .../client/src/components/MkSuperMenu.vue | 5 +- .../src/components/MkUserSelectDialog.vue | 2 + .../client/src/components/MkUsersTooltip.vue | 2 +- packages/client/src/components/MkWidgets.vue | 2 +- .../client/src/components/form/folder.vue | 4 +- packages/client/src/components/form/radio.vue | 3 + .../client/src/components/form/switch.vue | 3 + .../src/components/global/MkPageHeader.vue | 2 + .../src/components/global/RouterView.vue | 3 + packages/client/src/directives/focus.ts | 3 + packages/client/src/directives/index.ts | 2 + packages/client/src/directives/tooltip.ts | 33 +- packages/client/src/pages/admin/_header_.vue | 6 +- .../src/pages/admin/overview.moderators.vue | 2 +- packages/client/src/pages/follow-requests.vue | 1 + .../client/src/pages/settings/accounts.vue | 8 +- packages/client/src/style.scss | 4 - .../src/ui/_common_/navbar-for-mobile.vue | 1 + packages/client/src/ui/_common_/navbar.vue | 21 +- packages/client/src/ui/classic.header.vue | 1 + packages/client/src/ui/classic.sidebar.vue | 4 +- packages/client/src/ui/classic.vue | 2 + pnpm-lock.yaml | 41 +- 41 files changed, 601 insertions(+), 493 deletions(-) create mode 100644 packages/client/src/directives/focus.ts diff --git a/package.json b/package.json index 63b5f1dcd..0b681f6d5 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,8 @@ "@bull-board/ui": "^4.10.2", "@napi-rs/cli": "^2.15.0", "@tensorflow/tfjs": "^3.21.0", + "focus-trap": "^7.2.0", + "focus-trap-vue": "^4.0.1", "js-yaml": "4.1.0", "seedrandom": "^3.0.5" }, diff --git a/packages/client/src/components/MkButton.vue b/packages/client/src/components/MkButton.vue index 5f1a5bdb7..feac281d9 100644 --- a/packages/client/src/components/MkButton.vue +++ b/packages/client/src/components/MkButton.vue @@ -195,8 +195,7 @@ function onMousedown(evt: MouseEvent): void { } &:focus-visible { - outline: solid 2px var(--focus); - outline-offset: 2px; + outline: auto; } &.inline { diff --git a/packages/client/src/components/MkCwButton.vue b/packages/client/src/components/MkCwButton.vue index 659cb1fbb..1f6340510 100644 --- a/packages/client/src/components/MkCwButton.vue +++ b/packages/client/src/components/MkCwButton.vue @@ -1,5 +1,6 @@ <template> <button + ref="el" class="_button" :class="{ showLess: modelValue, fade: !modelValue }" @click.stop="toggle" @@ -12,7 +13,7 @@ </template> <script lang="ts" setup> -import { computed } from "vue"; +import { computed, ref } from "vue"; import { length } from "stringz"; import * as misskey from "calckey-js"; import { concat } from "@/scripts/array"; @@ -27,6 +28,8 @@ const emit = defineEmits<{ (ev: "update:modelValue", v: boolean): void; }>(); +const el = ref<HTMLElement>(); + const label = computed(() => { return concat([ props.note.text @@ -43,6 +46,14 @@ const label = computed(() => { const toggle = () => { emit("update:modelValue", !props.modelValue); }; + +function focus() { + el.value.focus(); +} + +defineExpose({ + focus +}); </script> <style lang="scss" scoped> @@ -62,7 +73,7 @@ const toggle = () => { } } } - &:hover > span { + &:hover > span, &:focus > span { background: var(--cwFg) !important; color: var(--cwBg) !important; } @@ -73,6 +84,7 @@ const toggle = () => { bottom: 0; left: 0; width: 100%; + z-index: 2; > span { display: inline-block; background: var(--panel); @@ -81,7 +93,7 @@ const toggle = () => { border-radius: 999px; box-shadow: 0 2px 6px rgb(0 0 0 / 20%); } - &:hover { + &:hover, &:focus { > span { background: var(--panelHighlight); } diff --git a/packages/client/src/components/MkDriveFileThumbnail.vue b/packages/client/src/components/MkDriveFileThumbnail.vue index 39150c10c..48b542817 100644 --- a/packages/client/src/components/MkDriveFileThumbnail.vue +++ b/packages/client/src/components/MkDriveFileThumbnail.vue @@ -1,5 +1,5 @@ <template> - <div ref="thumbnail" class="zdjebgpv"> + <button ref="thumbnail" class="zdjebgpv"> <ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" @@ -36,7 +36,7 @@ v-if="isThumbnailAvailable && is === 'video'" class="ph-file-video ph-bold ph-lg icon-sub" ></i> - </div> + </button> </template> <script lang="ts" setup> @@ -88,6 +88,9 @@ const isThumbnailAvailable = computed(() => { background: var(--panel); border-radius: 8px; overflow: clip; + border: 0; + padding: 0; + cursor: pointer; > .icon-sub { position: absolute; diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue index a22006951..88d207bab 100644 --- a/packages/client/src/components/MkEmojiPicker.vue +++ b/packages/client/src/components/MkEmojiPicker.vue @@ -1,157 +1,160 @@ <template> - <div - class="omfetrab" - :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" - :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }" - > - <input - ref="search" - v-model.trim="q" - class="search" - data-prevent-emoji-insert - :class="{ filled: q != null && q != '' }" - :placeholder="i18n.ts.search" - type="search" - @paste.stop="paste" - @keyup.enter="done()" - /> - <div ref="emojis" class="emojis"> - <section class="result"> - <div v-if="searchResultCustom.length > 0" class="body"> - <button - v-for="emoji in searchResultCustom" - :key="emoji.id" - class="_button item" - :title="emoji.name" - tabindex="0" - @click="chosen(emoji, $event)" - > - <!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>--> - <img - class="emoji" - :src=" - disableShowingAnimatedImages - ? getStaticImageUrl(emoji.url) - : emoji.url - " - /> - </button> - </div> - <div v-if="searchResultUnicode.length > 0" class="body"> - <button - v-for="emoji in searchResultUnicode" - :key="emoji.name" - class="_button item" - :title="emoji.name" - tabindex="0" - @click="chosen(emoji, $event)" - > - <MkEmoji class="emoji" :emoji="emoji.char" /> - </button> - </div> - </section> - - <div v-if="tab === 'index'" class="group index"> - <section v-if="showPinned"> - <div class="body"> + <FocusTrap v-bind:active="isActive"> + <div + class="omfetrab" + :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" + :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }" + tabindex="-1" + > + <input + ref="search" + v-model.trim="q" + class="search" + data-prevent-emoji-insert + :class="{ filled: q != null && q != '' }" + :placeholder="i18n.ts.search" + type="search" + @paste.stop="paste" + @keyup.enter="done()" + /> + <div ref="emojis" class="emojis"> + <section class="result"> + <div v-if="searchResultCustom.length > 0" class="body"> <button - v-for="emoji in pinned" - :key="emoji" + v-for="emoji in searchResultCustom" + :key="emoji.id" class="_button item" + :title="emoji.name" tabindex="0" @click="chosen(emoji, $event)" > - <MkEmoji + <!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>--> + <img class="emoji" - :emoji="emoji" - :normal="true" + :src=" + disableShowingAnimatedImages + ? getStaticImageUrl(emoji.url) + : emoji.url + " /> </button> </div> + <div v-if="searchResultUnicode.length > 0" class="body"> + <button + v-for="emoji in searchResultUnicode" + :key="emoji.name" + class="_button item" + :title="emoji.name" + tabindex="0" + @click="chosen(emoji, $event)" + > + <MkEmoji class="emoji" :emoji="emoji.char" /> + </button> + </div> </section> - <section> - <header class="_acrylic"> - <i class="ph-alarm ph-bold ph-fw ph-lg"></i> - {{ i18n.ts.recentUsed }} - </header> - <div class="body"> - <button - v-for="emoji in recentlyUsedEmojis" - :key="emoji" - class="_button item" - @click="chosen(emoji, $event)" - > - <MkEmoji - class="emoji" - :emoji="emoji" - :normal="true" - /> - </button> - </div> - </section> + <div v-if="tab === 'index'" class="group index"> + <section v-if="showPinned"> + <div class="body"> + <button + v-for="emoji in pinned" + :key="emoji" + class="_button item" + tabindex="0" + @click="chosen(emoji, $event)" + > + <MkEmoji + class="emoji" + :emoji="emoji" + :normal="true" + /> + </button> + </div> + </section> + + <section> + <header class="_acrylic"> + <i class="ph-alarm ph-bold ph-fw ph-lg"></i> + {{ i18n.ts.recentUsed }} + </header> + <div class="body"> + <button + v-for="emoji in recentlyUsedEmojis" + :key="emoji" + class="_button item" + @click="chosen(emoji, $event)" + > + <MkEmoji + class="emoji" + :emoji="emoji" + :normal="true" + /> + </button> + </div> + </section> + </div> + <div v-once class="group"> + <header>{{ i18n.ts.customEmojis }}</header> + <XSection + v-for="category in customEmojiCategories" + :key="'custom:' + category" + :initial-shown="false" + :emojis=" + customEmojis + .filter((e) => e.category === category) + .map((e) => ':' + e.name + ':') + " + @chosen="chosen" + >{{ category || i18n.ts.other }}</XSection + > + </div> + <div v-once class="group"> + <header>{{ i18n.ts.emoji }}</header> + <XSection + v-for="category in categories" + :key="category" + :emojis=" + emojilist + .filter((e) => e.category === category) + .map((e) => e.char) + " + @chosen="chosen" + >{{ category }}</XSection + > + </div> </div> - <div v-once class="group"> - <header>{{ i18n.ts.customEmojis }}</header> - <XSection - v-for="category in customEmojiCategories" - :key="'custom:' + category" - :initial-shown="false" - :emojis=" - customEmojis - .filter((e) => e.category === category) - .map((e) => ':' + e.name + ':') - " - @chosen="chosen" - >{{ category || i18n.ts.other }}</XSection + <div class="tabs"> + <button + class="_button tab" + :class="{ active: tab === 'index' }" + @click="tab = 'index'" > - </div> - <div v-once class="group"> - <header>{{ i18n.ts.emoji }}</header> - <XSection - v-for="category in categories" - :key="category" - :emojis=" - emojilist - .filter((e) => e.category === category) - .map((e) => e.char) - " - @chosen="chosen" - >{{ category }}</XSection + <i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i> + </button> + <button + class="_button tab" + :class="{ active: tab === 'custom' }" + @click="tab = 'custom'" > + <i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i> + </button> + <button + class="_button tab" + :class="{ active: tab === 'unicode' }" + @click="tab = 'unicode'" + > + <i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i> + </button> + <button + class="_button tab" + :class="{ active: tab === 'tags' }" + @click="tab = 'tags'" + > + <i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i> + </button> </div> </div> - <div class="tabs"> - <button - class="_button tab" - :class="{ active: tab === 'index' }" - @click="tab = 'index'" - > - <i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i> - </button> - <button - class="_button tab" - :class="{ active: tab === 'custom' }" - @click="tab = 'custom'" - > - <i class="ph-smiley ph-bold ph-lg ph-fw ph-lg"></i> - </button> - <button - class="_button tab" - :class="{ active: tab === 'unicode' }" - @click="tab = 'unicode'" - > - <i class="ph-leaf ph-bold ph-lg ph-fw ph-lg"></i> - </button> - <button - class="_button tab" - :class="{ active: tab === 'tags' }" - @click="tab = 'tags'" - > - <i class="ph-hash ph-bold ph-lg ph-fw ph-lg"></i> - </button> - </div> - </div> + </FocusTrap> </template> <script lang="ts" setup> @@ -171,6 +174,7 @@ import { deviceKind } from "@/scripts/device-kind"; import { emojiCategories, instance } from "@/instance"; import { i18n } from "@/i18n"; import { defaultStore } from "@/store"; +import { FocusTrap } from 'focus-trap-vue'; const props = withDefaults( defineProps<{ diff --git a/packages/client/src/components/MkLaunchPad.vue b/packages/client/src/components/MkLaunchPad.vue index f713b4c41..759c215f7 100644 --- a/packages/client/src/components/MkLaunchPad.vue +++ b/packages/client/src/components/MkLaunchPad.vue @@ -139,7 +139,7 @@ function close() { height: 100px; border-radius: 10px; - &:hover { + &:hover, &:focus-visible { color: var(--accent); background: var(--accentedBg); text-decoration: none; diff --git a/packages/client/src/components/MkMediaImage.vue b/packages/client/src/components/MkMediaImage.vue index 882908040..3cfb0f465 100644 --- a/packages/client/src/components/MkMediaImage.vue +++ b/packages/client/src/components/MkMediaImage.vue @@ -138,6 +138,10 @@ watch( background-position: center; background-size: contain; background-repeat: no-repeat; + box-sizing: border-box; + &:focus-visible { + border: 2px solid var(--accent); + } > .gif { background-color: var(--fg); diff --git a/packages/client/src/components/MkMenu.child.vue b/packages/client/src/components/MkMenu.child.vue index 6b05ab447..e5ca9e4ee 100644 --- a/packages/client/src/components/MkMenu.child.vue +++ b/packages/client/src/components/MkMenu.child.vue @@ -1,14 +1,14 @@ <template> - <div ref="el" class="sfhdhdhr"> - <MkMenu - ref="menu" - :items="items" - :align="align" - :width="width" - :as-drawer="false" - @close="onChildClosed" - /> - </div> + <div ref="el" class="sfhdhdhr" tabindex="-1"> + <MkMenu + ref="menu" + :items="items" + :align="align" + :width="width" + :as-drawer="false" + @close="onChildClosed" + /> + </div> </template> <script lang="ts" setup> @@ -23,7 +23,6 @@ import { } from "vue"; import MkMenu from "./MkMenu.vue"; import { MenuItem } from "@/types/menu"; -import * as os from "@/os"; const props = defineProps<{ items: MenuItem[]; diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue index 88c8af1c5..c71e3ac58 100644 --- a/packages/client/src/components/MkMenu.vue +++ b/packages/client/src/components/MkMenu.vue @@ -1,191 +1,188 @@ <template> - <div> - <div - ref="itemsEl" - v-hotkey="keymap" - class="rrevdjwt _popup _shadow" - :class="{ center: align === 'center', asDrawer }" - :style="{ - width: width && !asDrawer ? width + 'px' : '', - maxHeight: maxHeight ? maxHeight + 'px' : '', - }" - @contextmenu.self="(e) => e.preventDefault()" - > - <template v-for="(item, i) in items2"> - <div v-if="item === null" class="divider"></div> - <span v-else-if="item.type === 'label'" class="label item"> - <span :style="item.textStyle || ''">{{ item.text }}</span> - </span> - <span - v-else-if="item.type === 'pending'" - :tabindex="i" - class="pending item" - > - <span><MkEllipsis /></span> - </span> - <MkA - v-else-if="item.type === 'link'" - :to="item.to" - :tabindex="i" - class="_button item" - @click.passive="close(true)" - @mouseenter.passive="onItemMouseEnter(item)" - @mouseleave.passive="onItemMouseLeave(item)" - > - <i - v-if="item.icon" - class="ph-fw ph-lg" - :class="item.icon" - ></i> - <span v-else-if="item.icons"> - <i - v-for="icon in item.icons" - class="ph-fw ph-lg" - :class="icon" - ></i> + <FocusTrap v-bind:active="isActive"> + <div tabindex="-1" v-focus> + <div + ref="itemsEl" + class="rrevdjwt _popup _shadow" + :class="{ center: align === 'center', asDrawer }" + :style="{ + width: width && !asDrawer ? width + 'px' : '', + maxHeight: maxHeight ? maxHeight + 'px' : '', + }" + @contextmenu.self="(e) => e.preventDefault()" + > + <template v-for="(item, i) in items2"> + <div v-if="item === null" class="divider"></div> + <span v-else-if="item.type === 'label'" class="label item"> + <span :style="item.textStyle || ''">{{ item.text }}</span> </span> - <MkAvatar - v-if="item.avatar" - :user="item.avatar" - class="avatar" - /> - <span :style="item.textStyle || ''">{{ item.text }}</span> - <span v-if="item.indicate" class="indicator" - ><i class="ph-circle ph-fill"></i - ></span> - </MkA> - <a - v-else-if="item.type === 'a'" - :href="item.href" - :target="item.target" - :download="item.download" - :tabindex="i" - class="_button item" - @click="close(true)" - @mouseenter.passive="onItemMouseEnter(item)" - @mouseleave.passive="onItemMouseLeave(item)" - > - <i - v-if="item.icon" - class="ph-fw ph-lg" - :class="item.icon" - ></i> - <span v-else-if="item.icons"> - <i - v-for="icon in item.icons" - class="ph-fw ph-lg" - :class="icon" - ></i> - </span> - <span :style="item.textStyle || ''">{{ item.text }}</span> - <span v-if="item.indicate" class="indicator" - ><i class="ph-circle ph-fill"></i - ></span> - </a> - <button - v-else-if="item.type === 'user' && !items.hidden" - :tabindex="i" - class="_button item" - :class="{ active: item.active }" - :disabled="item.active" - @click="clicked(item.action, $event)" - @mouseenter.passive="onItemMouseEnter(item)" - @mouseleave.passive="onItemMouseLeave(item)" - > - <MkAvatar :user="item.user" class="avatar" /><MkUserName - :user="item.user" - /> - <span v-if="item.indicate" class="indicator" - ><i class="ph-circle ph-fill"></i - ></span> - </button> - <span - v-else-if="item.type === 'switch'" - :tabindex="i" - class="item" - @mouseenter.passive="onItemMouseEnter(item)" - @mouseleave.passive="onItemMouseLeave(item)" - > - <FormSwitch - v-model="item.ref" - :disabled="item.disabled" - class="form-switch" - :style="item.textStyle || ''" - >{{ item.text }}</FormSwitch + <span + v-else-if="item.type === 'pending'" + class="pending item" > + <span><MkEllipsis /></span> + </span> + <MkA + v-else-if="item.type === 'link'" + :to="item.to" + class="_button item" + @click.passive="close(true)" + @mouseenter.passive="onItemMouseEnter(item)" + @mouseleave.passive="onItemMouseLeave(item)" + > + <i + v-if="item.icon" + class="ph-fw ph-lg" + :class="item.icon" + ></i> + <span v-else-if="item.icons"> + <i + v-for="icon in item.icons" + class="ph-fw ph-lg" + :class="icon" + ></i> + </span> + <MkAvatar + v-if="item.avatar" + :user="item.avatar" + class="avatar" + disableLink + /> + <span :style="item.textStyle || ''">{{ item.text }}</span> + <span v-if="item.indicate" class="indicator" + ><i class="ph-circle ph-fill"></i + ></span> + </MkA> + <a + v-else-if="item.type === 'a'" + :href="item.href" + :target="item.target" + :download="item.download" + class="_button item" + @click="close(true)" + @mouseenter.passive="onItemMouseEnter(item)" + @mouseleave.passive="onItemMouseLeave(item)" + > + <i + v-if="item.icon" + class="ph-fw ph-lg" + :class="item.icon" + ></i> + <span v-else-if="item.icons"> + <i + v-for="icon in item.icons" + class="ph-fw ph-lg" + :class="icon" + ></i> + </span> + <span :style="item.textStyle || ''">{{ item.text }}</span> + <span v-if="item.indicate" class="indicator" + ><i class="ph-circle ph-fill"></i + ></span> + </a> + <button + v-else-if="item.type === 'user' && !items.hidden" + class="_button item" + :class="{ active: item.active }" + :disabled="item.active" + @click="clicked(item.action, $event)" + @mouseenter.passive="onItemMouseEnter(item)" + @mouseleave.passive="onItemMouseLeave(item)" + > + <MkAvatar :user="item.user" class="avatar" disableLink /><MkUserName + :user="item.user" + /> + <span v-if="item.indicate" class="indicator" + ><i class="ph-circle ph-fill"></i + ></span> + </button> + <span + v-else-if="item.type === 'switch'" + class="item" + @mouseenter.passive="onItemMouseEnter(item)" + @mouseleave.passive="onItemMouseLeave(item)" + > + <FormSwitch + v-model="item.ref" + :disabled="item.disabled" + class="form-switch" + :style="item.textStyle || ''" + >{{ item.text }}</FormSwitch + > + </span> + <button + v-else-if="item.type === 'parent'" + class="_button item parent" + :class="{ childShowing: childShowingItem === item }" + @mouseenter="showChildren(item, $event)" + @click="showChildren(item, $event)" + > + <i + v-if="item.icon" + class="ph-fw ph-lg" + :class="item.icon" + ></i> + <span v-else-if="item.icons"> + <i + v-for="icon in item.icons" + class="ph-fw ph-lg" + :class="icon" + ></i> + </span> + <span :style="item.textStyle || ''">{{ item.text }}</span> + <span class="caret" + ><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i + ></span> + </button> + <button + v-else-if="!item.hidden" + class="_button item" + :class="{ danger: item.danger, active: item.active }" + :disabled="item.active" + @click="clicked(item.action, $event)" + @mouseenter.passive="onItemMouseEnter(item)" + @mouseleave.passive="onItemMouseLeave(item)" + > + <i + v-if="item.icon" + class="ph-fw ph-lg" + :class="item.icon" + ></i> + <span v-else-if="item.icons"> + <i + v-for="icon in item.icons" + class="ph-fw ph-lg" + :class="icon" + ></i> + </span> + <MkAvatar + v-if="item.avatar" + :user="item.avatar" + class="avatar" + disableLink + /> + <span :style="item.textStyle || ''">{{ item.text }}</span> + <span v-if="item.indicate" class="indicator" + ><i class="ph-circle ph-fill"></i + ></span> + </button> + </template> + <span v-if="items2.length === 0" class="none item"> + <span>{{ i18n.ts.none }}</span> </span> - <button - v-else-if="item.type === 'parent'" - :tabindex="i" - class="_button item parent" - :class="{ childShowing: childShowingItem === item }" - @mouseenter="showChildren(item, $event)" - > - <i - v-if="item.icon" - class="ph-fw ph-lg" - :class="item.icon" - ></i> - <span v-else-if="item.icons"> - <i - v-for="icon in item.icons" - class="ph-fw ph-lg" - :class="icon" - ></i> - </span> - <span :style="item.textStyle || ''">{{ item.text }}</span> - <span class="caret" - ><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i - ></span> - </button> - <button - v-else-if="!item.hidden" - :tabindex="i" - class="_button item" - :class="{ danger: item.danger, active: item.active }" - :disabled="item.active" - @click="clicked(item.action, $event)" - @mouseenter.passive="onItemMouseEnter(item)" - @mouseleave.passive="onItemMouseLeave(item)" - > - <i - v-if="item.icon" - class="ph-fw ph-lg" - :class="item.icon" - ></i> - <span v-else-if="item.icons"> - <i - v-for="icon in item.icons" - class="ph-fw ph-lg" - :class="icon" - ></i> - </span> - <MkAvatar - v-if="item.avatar" - :user="item.avatar" - class="avatar" - /> - <span :style="item.textStyle || ''">{{ item.text }}</span> - <span v-if="item.indicate" class="indicator" - ><i class="ph-circle ph-fill"></i - ></span> - </button> - </template> - <span v-if="items2.length === 0" class="none item"> - <span>{{ i18n.ts.none }}</span> - </span> + </div> + <div v-if="childMenu" class="child"> + <XChild + ref="child" + :items="childMenu" + :target-element="childTarget" + :root-element="itemsEl" + showing + @actioned="childActioned" + /> + </div> </div> - <div v-if="childMenu" class="child"> - <XChild - ref="child" - :items="childMenu" - :target-element="childTarget" - :root-element="itemsEl" - showing - @actioned="childActioned" - /> - </div> - </div> + </FocusTrap> </template> <script lang="ts" setup> @@ -206,6 +203,7 @@ import FormSwitch from "@/components/form/switch.vue"; import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from "@/types/menu"; import * as os from "@/os"; import { i18n } from "@/i18n"; +import { FocusTrap } from 'focus-trap-vue'; const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue")); @@ -228,12 +226,6 @@ let items2: InnerMenuItem[] = $ref([]); let child = $ref<InstanceType<typeof XChild>>(); -let keymap = computed(() => ({ - "up|k|shift+tab": focusUp, - "down|j|tab": focusDown, - esc: close, -})); - let childShowingItem = $ref<MenuItem | null>(); watch( @@ -364,8 +356,7 @@ onBeforeUnmount(() => { font-size: 0.9em; line-height: 20px; text-align: left; - overflow: hidden; - text-overflow: ellipsis; + outline: none; &:before { content: ""; @@ -389,7 +380,7 @@ onBeforeUnmount(() => { transform: translateY(0em); } - &:not(:disabled):hover { + &:not(:disabled):hover, &:focus-visible { color: var(--accent); text-decoration: none; @@ -397,6 +388,9 @@ onBeforeUnmount(() => { background: var(--accentedBg); } } + &:focus-visible:before { + outline: auto; + } &.danger { color: #eb6f92; diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue index d9cd56f95..12e79f428 100644 --- a/packages/client/src/components/MkModal.vue +++ b/packages/client/src/components/MkModal.vue @@ -14,54 +14,59 @@ :duration="transitionDuration" appear @after-leave="emit('closed')" + @keyup.esc="emit('click')" @enter="emit('opening')" @after-enter="onOpened" > - <div - v-show="manualShowing != null ? manualShowing : showing" - v-hotkey.global="keymap" - :class="[ - $style.root, - { - [$style.drawer]: type === 'drawer', - [$style.dialog]: type === 'dialog' || type === 'dialog:top', - [$style.popup]: type === 'popup', - }, - ]" - :style="{ - zIndex, - pointerEvents: (manualShowing != null ? manualShowing : showing) - ? 'auto' - : 'none', - '--transformOrigin': transformOrigin, - }" - > + <FocusTrap v-model:active="isActive"> <div - class="_modalBg data-cy-bg" + v-show="manualShowing != null ? manualShowing : showing" + v-hotkey.global="keymap" :class="[ - $style.bg, + $style.root, { - [$style.bgTransparent]: isEnableBgTransparent, - 'data-cy-transparent': isEnableBgTransparent, + [$style.drawer]: type === 'drawer', + [$style.dialog]: type === 'dialog' || type === 'dialog:top', + [$style.popup]: type === 'popup', }, ]" - :style="{ zIndex }" - @click="onBgClick" - @mousedown="onBgClick" - @contextmenu.prevent.stop="() => {}" - ></div> - <div - ref="content" - :class="[ - $style.content, - { [$style.fixed]: fixed, top: type === 'dialog:top' }, - ]" - :style="{ zIndex }" - @click.self="onBgClick" + :style="{ + zIndex, + pointerEvents: (manualShowing != null ? manualShowing : showing) + ? 'auto' + : 'none', + '--transformOrigin': transformOrigin, + }" + tabindex="-1" + v-focus > - <slot :max-height="maxHeight" :type="type"></slot> + <div + class="_modalBg data-cy-bg" + :class="[ + $style.bg, + { + [$style.bgTransparent]: isEnableBgTransparent, + 'data-cy-transparent': isEnableBgTransparent, + }, + ]" + :style="{ zIndex }" + @click="onBgClick" + @mousedown="onBgClick" + @contextmenu.prevent.stop="() => {}" + ></div> + <div + ref="content" + :class="[ + $style.content, + { [$style.fixed]: fixed, top: type === 'dialog:top' }, + ]" + :style="{ zIndex }" + @click.self="onBgClick" + > + <slot :max-height="maxHeight" :type="type"></slot> + </div> </div> - </div> + </FocusTrap> </Transition> </template> @@ -71,6 +76,7 @@ import * as os from "@/os"; import { isTouchUsing } from "@/scripts/touch"; import { defaultStore } from "@/store"; import { deviceKind } from "@/scripts/device-kind"; +import { FocusTrap } from 'focus-trap-vue'; function getFixedContainer(el: Element | null): Element | null { if (el == null || el.tagName === "BODY") return null; @@ -166,6 +172,7 @@ let transitionDuration = $computed(() => let contentClicking = false; +const focusedElement = document.activeElement; function close(opts: { useSendAnimation?: boolean } = {}) { if (opts.useSendAnimation) { useSendAnime = true; @@ -175,10 +182,12 @@ function close(opts: { useSendAnimation?: boolean } = {}) { if (props.src) props.src.style.pointerEvents = "auto"; showing = false; emit("close"); + focusedElement.focus(); } function onBgClick() { if (contentClicking) return; + focusedElement.focus(); emit("click"); } @@ -481,6 +490,7 @@ defineExpose({ } .root { + outline: none; &.dialog { > .content { position: fixed; diff --git a/packages/client/src/components/MkModalPageWindow.vue b/packages/client/src/components/MkModalPageWindow.vue index 361128464..bf4d8d0bc 100644 --- a/packages/client/src/components/MkModalPageWindow.vue +++ b/packages/client/src/components/MkModalPageWindow.vue @@ -158,6 +158,7 @@ function onContextmenu(ev: MouseEvent) { flex-direction: column; contain: content; border-radius: var(--radius); + margin: auto; --root-margin: 24px; diff --git a/packages/client/src/components/MkModalWindow.vue b/packages/client/src/components/MkModalWindow.vue index 3afcff6cb..017bfae8c 100644 --- a/packages/client/src/components/MkModalWindow.vue +++ b/packages/client/src/components/MkModalWindow.vue @@ -3,59 +3,64 @@ ref="modal" :prefer-type="'dialog'" @click="onBgClick" + @keyup.esc="$emit('close')" @closed="$emit('closed')" > - <div - ref="rootEl" - class="ebkgoccj" - :style="{ - width: `${width}px`, - height: scroll - ? height - ? `${height}px` - : null - : height - ? `min(${height}px, 100%)` - : '100%', - }" - @keydown="onKeydown" - > - <div ref="headerEl" class="header"> - <button - v-if="withOkButton" - class="_button" - @click="$emit('close')" - > - <i class="ph-x ph-bold ph-lg"></i> - </button> - <span class="title"> - <slot name="header"></slot> - </span> - <button - v-if="!withOkButton" - class="_button" - @click="$emit('close')" - > - <i class="ph-x ph-bold ph-lg"></i> - </button> - <button - v-if="withOkButton" - class="_button" - :disabled="okButtonDisabled" - @click="$emit('ok')" - > - <i class="ph-check ph-bold ph-lg"></i> - </button> + <FocusTrap v-model:active="isActive"> + <div + ref="rootEl" + class="ebkgoccj" + :style="{ + width: `${width}px`, + height: scroll + ? height + ? `${height}px` + : null + : height + ? `min(${height}px, 100%)` + : '100%', + }" + @keydown="onKeydown" + tabindex="-1" + > + <div ref="headerEl" class="header"> + <button + v-if="withOkButton" + class="_button" + @click="$emit('close')" + > + <i class="ph-x ph-bold ph-lg"></i> + </button> + <span class="title"> + <slot name="header"></slot> + </span> + <button + v-if="!withOkButton" + class="_button" + @click="$emit('close')" + > + <i class="ph-x ph-bold ph-lg"></i> + </button> + <button + v-if="withOkButton" + class="_button" + :disabled="okButtonDisabled" + @click="$emit('ok')" + > + <i class="ph-check ph-bold ph-lg"></i> + </button> + </div> + <div class="body"> + <slot :width="bodyWidth" :height="bodyHeight"></slot> + </div> </div> - <div class="body"> - <slot :width="bodyWidth" :height="bodyHeight"></slot> - </div> - </div> + </FocusTrap> </MkModal> </template> <script lang="ts" setup> import { onMounted, onUnmounted } from "vue"; +import { FocusTrap } from 'focus-trap-vue'; import MkModal from "./MkModal.vue"; const props = withDefaults( diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue index 22a7ef93f..5d9c40d38 100644 --- a/packages/client/src/components/MkNote.vue +++ b/packages/client/src/components/MkNote.vue @@ -84,6 +84,7 @@ :detailedView="detailedView" :parentId="appearNote.parentId" @push="(e) => router.push(notePage(e))" + @focusfooter="footerEl.focus()" ></MkSubNoteContent> <div v-if="translating || translation" class="translation"> <MkLoading v-if="translating" mini /> @@ -117,7 +118,7 @@ <MkTime :time="appearNote.createdAt" mode="absolute" /> </MkA> </div> - <footer ref="el" class="footer" @click.stop> + <footer ref="footerEl" class="footer" @click.stop tabindex="-1"> <XReactionsViewer v-if="enableEmojiReactions" ref="reactionsViewer" @@ -278,6 +279,7 @@ const isRenote = note.poll == null; const el = ref<HTMLElement>(); +const footerEl = ref<HTMLElement>(); const menuButton = ref<HTMLElement>(); const starButton = ref<InstanceType<typeof XStarButton>>(); const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); @@ -298,8 +300,8 @@ const keymap = { r: () => reply(true), "e|a|plus": () => react(true), q: () => renoteButton.value.renote(true), - "up|k|shift+tab": focusBefore, - "down|j|tab": focusAfter, + "up|k": focusBefore, + "down|j": focusAfter, esc: blur, "m|o": () => menu(true), s: () => showContent.value !== showContent.value, diff --git a/packages/client/src/components/MkNotePreview.vue b/packages/client/src/components/MkNotePreview.vue index 9d388e71b..6fdd79dc6 100644 --- a/packages/client/src/components/MkNotePreview.vue +++ b/packages/client/src/components/MkNotePreview.vue @@ -1,6 +1,6 @@ <template> <div v-size="{ min: [350, 500] }" class="fefdfafb"> - <MkAvatar class="avatar" :user="$i" /> + <MkAvatar class="avatar" :user="$i" disableLink /> <div class="main"> <div class="header"> <MkUserName :user="$i" /> diff --git a/packages/client/src/components/MkNoteSub.vue b/packages/client/src/components/MkNoteSub.vue index f5e70891f..a0b70ff1f 100644 --- a/packages/client/src/components/MkNoteSub.vue +++ b/packages/client/src/components/MkNoteSub.vue @@ -26,6 +26,7 @@ :note="note" :parentId="appearNote.parentId" :conversation="conversation" + @focusfooter="footerEl.focus()" /> <div v-if="translating || translation" class="translation"> <MkLoading v-if="translating" mini /> @@ -46,7 +47,7 @@ </div> </div> </div> - <footer class="footer" @click.stop> + <footer ref="footerEl" class="footer" @click.stop tabindex="-1"> <XReactionsViewer v-if="enableEmojiReactions" ref="reactionsViewer" @@ -212,6 +213,7 @@ const isRenote = note.poll == null; const el = ref<HTMLElement>(); +const footerEl = ref<HTMLElement>(); const menuButton = ref<HTMLElement>(); const starButton = ref<InstanceType<typeof XStarButton>>(); const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); diff --git a/packages/client/src/components/MkPopupMenu.vue b/packages/client/src/components/MkPopupMenu.vue index 4d52616e1..5f1ed037b 100644 --- a/packages/client/src/components/MkPopupMenu.vue +++ b/packages/client/src/components/MkPopupMenu.vue @@ -7,6 +7,8 @@ :transparent-bg="true" @click="modal.close()" @closed="emit('closed')" + tabindex="-1" + v-focus > <MkMenu :items="items" diff --git a/packages/client/src/components/MkPostFormAttaches.vue b/packages/client/src/components/MkPostFormAttaches.vue index 7c7f240e8..7cf397e55 100644 --- a/packages/client/src/components/MkPostFormAttaches.vue +++ b/packages/client/src/components/MkPostFormAttaches.vue @@ -198,7 +198,6 @@ export default defineComponent({ height: 64px; margin-right: 4px; border-radius: 4px; - overflow: hidden; cursor: move; &:hover > .remove { diff --git a/packages/client/src/components/MkSubNoteContent.vue b/packages/client/src/components/MkSubNoteContent.vue index a1f7cc1b9..68439527a 100644 --- a/packages/client/src/components/MkSubNoteContent.vue +++ b/packages/client/src/components/MkSubNoteContent.vue @@ -35,7 +35,11 @@ class="content" :class="{ collapsed, isLong, showContent: note.cw && !showContent }" > - <div class="body"> + <XCwButton ref="cwButton" v-if="note.cw && !showContent" v-model="showContent" :note="note" v-on:keydown="focusFooter" /> + <div + class="body" + v-bind="{ 'aria-label': !showContent ? '' : null, 'tabindex': !showContent ? '-1' : null }" + > <span v-if="note.deletedAt" style="opacity: 0.5" >({{ i18n.ts.deleted }})</span > @@ -96,15 +100,20 @@ <XNoteSimple :note="note.renote" /> </div> </template> + <div + v-if="note.cw && !showContent" + tabindex="0" + v-on:focus="cwButton?.focus()" + ></div> </div> <XShowMoreButton v-if="isLong" v-model="collapsed"></XShowMoreButton> - <XCwButton v-if="note.cw" v-model="showContent" :note="note" /> + <XCwButton v-if="note.cw && showContent" v-model="showContent" :note="note" /> </div> </div> </template> <script lang="ts" setup> -import {} from "vue"; +import { ref } from "vue"; import * as misskey from "calckey-js"; import * as mfm from "mfm-js"; import XNoteSimple from "@/components/MkNoteSimple.vue"; @@ -126,8 +135,10 @@ const props = defineProps<{ const emit = defineEmits<{ (ev: "push", v): void; + (ev: "focusfooter"): void; }>(); +const cwButton = ref<HTMLElement>(); const isLong = !props.detailedView && props.note.cw == null && @@ -140,6 +151,13 @@ const urls = props.note.text : null; let showContent = $ref(false); + + +function focusFooter(ev) { + if (ev.key == "Tab" && !ev.getModifierState("Shift")) { + emit("focusfooter"); + } +} </script> <style lang="scss" scoped> @@ -231,6 +249,9 @@ let showContent = $ref(false); margin-top: -50px; padding-top: 50px; overflow: hidden; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; } &.collapsed > .body { box-sizing: border-box; diff --git a/packages/client/src/components/MkSuperMenu.vue b/packages/client/src/components/MkSuperMenu.vue index 55d6fba50..83c667070 100644 --- a/packages/client/src/components/MkSuperMenu.vue +++ b/packages/client/src/components/MkSuperMenu.vue @@ -9,7 +9,6 @@ v-if="item.type === 'a'" :href="item.href" :target="item.target" - :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" > @@ -22,7 +21,6 @@ </a> <button v-else-if="item.type === 'button'" - :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @@ -38,7 +36,6 @@ <MkA v-else :to="item.to" - :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" > @@ -99,7 +96,7 @@ export default defineComponent({ font-size: 0.9em; margin-bottom: 0.3rem; - &:hover { + &:hover, &:focus-visible { text-decoration: none; background: var(--panelHighlight); } diff --git a/packages/client/src/components/MkUserSelectDialog.vue b/packages/client/src/components/MkUserSelectDialog.vue index 506f48bd4..14553ca46 100644 --- a/packages/client/src/components/MkUserSelectDialog.vue +++ b/packages/client/src/components/MkUserSelectDialog.vue @@ -46,6 +46,7 @@ :user="user" class="avatar" :show-indicator="true" + disableLink /> <div class="body"> <MkUserName :user="user" class="name" /> @@ -73,6 +74,7 @@ :user="user" class="avatar" :show-indicator="true" + disableLink /> <div class="body"> <MkUserName :user="user" class="name" /> diff --git a/packages/client/src/components/MkUsersTooltip.vue b/packages/client/src/components/MkUsersTooltip.vue index 972864d1f..78a4f90f2 100644 --- a/packages/client/src/components/MkUsersTooltip.vue +++ b/packages/client/src/components/MkUsersTooltip.vue @@ -7,7 +7,7 @@ > <div class="beaffaef"> <div v-for="u in users" :key="u.id" class="user"> - <MkAvatar class="avatar" :user="u" /> + <MkAvatar class="avatar" :user="u" disableLink /> <MkUserName class="name" :user="u" :nowrap="true" /> </div> <div v-if="users.length < count" class="omitted"> diff --git a/packages/client/src/components/MkWidgets.vue b/packages/client/src/components/MkWidgets.vue index 07e845032..d48fc5383 100644 --- a/packages/client/src/components/MkWidgets.vue +++ b/packages/client/src/components/MkWidgets.vue @@ -1,7 +1,7 @@ <template> <div class="vjoppmmu"> <template v-if="edit"> - <header> + <header tabindex="-1" v-focus> <MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" diff --git a/packages/client/src/components/form/folder.vue b/packages/client/src/components/form/folder.vue index 8868f5784..a2fde5341 100644 --- a/packages/client/src/components/form/folder.vue +++ b/packages/client/src/components/form/folder.vue @@ -1,6 +1,6 @@ <template> <div class="dwzlatin" :class="{ opened }"> - <div class="header _button" @click="toggle"> + <button class="header _button" @click="toggle"> <span class="icon"><slot name="icon"></slot></span> <span class="text"><slot name="label"></slot></span> <span class="right"> @@ -8,7 +8,7 @@ <i v-if="opened" class="ph-caret-up ph-bold ph-lg icon"></i> <i v-else class="ph-caret-down ph-bold ph-lg icon"></i> </span> - </div> + </button> <KeepAlive> <div v-if="openedAtLeastOnce" v-show="opened" class="body"> <MkSpacer :margin-min="14" :margin-max="22"> diff --git a/packages/client/src/components/form/radio.vue b/packages/client/src/components/form/radio.vue index 493b2d010..ef644b327 100644 --- a/packages/client/src/components/form/radio.vue +++ b/packages/client/src/components/form/radio.vue @@ -66,6 +66,9 @@ function toggle(): void { &:hover { border-color: var(--inputBorderHover) !important; } + &:focus-within { + outline: auto; + } &.checked { background-color: var(--accentedBg) !important; diff --git a/packages/client/src/components/form/switch.vue b/packages/client/src/components/form/switch.vue index efaf488a9..b1c6df4e9 100644 --- a/packages/client/src/components/form/switch.vue +++ b/packages/client/src/components/form/switch.vue @@ -99,6 +99,9 @@ const toggle = () => { border-color: var(--inputBorderHover) !important; } } + &:focus-within > .button { + outline: auto; + } > .label { margin-left: 12px; diff --git a/packages/client/src/components/global/MkPageHeader.vue b/packages/client/src/components/global/MkPageHeader.vue index ad1d80ca6..c78ef0c10 100644 --- a/packages/client/src/components/global/MkPageHeader.vue +++ b/packages/client/src/components/global/MkPageHeader.vue @@ -19,6 +19,7 @@ class="avatar" :user="$i" :disable-preview="true" + disableLink /> </div> <template v-if="metadata"> @@ -33,6 +34,7 @@ :user="metadata.avatar" :disable-preview="true" :show-indicator="true" + disableLink /> <i v-else-if="metadata.icon && !narrow" diff --git a/packages/client/src/components/global/RouterView.vue b/packages/client/src/components/global/RouterView.vue index 8423ce773..437b7c53e 100644 --- a/packages/client/src/components/global/RouterView.vue +++ b/packages/client/src/components/global/RouterView.vue @@ -5,6 +5,9 @@ :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)" + tabindex="-1" + v-focus + style="outline: none;" /> <template #fallback> diff --git a/packages/client/src/directives/focus.ts b/packages/client/src/directives/focus.ts new file mode 100644 index 000000000..4d34fbf1f --- /dev/null +++ b/packages/client/src/directives/focus.ts @@ -0,0 +1,3 @@ +export default { + mounted: (el) => el.focus() +} diff --git a/packages/client/src/directives/index.ts b/packages/client/src/directives/index.ts index 0a5c32326..77639e2f3 100644 --- a/packages/client/src/directives/index.ts +++ b/packages/client/src/directives/index.ts @@ -11,6 +11,7 @@ import anim from "./anim"; import clickAnime from "./click-anime"; import panel from "./panel"; import adaptiveBorder from "./adaptive-border"; +import focus from "./focus"; export default function (app: App) { app.directive("userPreview", userPreview); @@ -25,4 +26,5 @@ export default function (app: App) { app.directive("click-anime", clickAnime); app.directive("panel", panel); app.directive("adaptive-border", adaptiveBorder); + app.directive("focus", focus); } diff --git a/packages/client/src/directives/tooltip.ts b/packages/client/src/directives/tooltip.ts index 7738d14e8..91024a6e3 100644 --- a/packages/client/src/directives/tooltip.ts +++ b/packages/client/src/directives/tooltip.ts @@ -76,23 +76,32 @@ export default { ev.preventDefault(); }); + function showTooltip() { + window.clearTimeout(self.showTimer); + window.clearTimeout(self.hideTimer); + self.showTimer = window.setTimeout(self.show, delay); + } + function hideTooltip() { + window.clearTimeout(self.showTimer); + window.clearTimeout(self.hideTimer); + self.hideTimer = window.setTimeout(self.close, delay); + } + el.addEventListener( - start, - () => { - window.clearTimeout(self.showTimer); - window.clearTimeout(self.hideTimer); - self.showTimer = window.setTimeout(self.show, delay); - }, + start, showTooltip, + { passive: true }, + ); + el.addEventListener( + "focusin", showTooltip, { passive: true }, ); el.addEventListener( - end, - () => { - window.clearTimeout(self.showTimer); - window.clearTimeout(self.hideTimer); - self.hideTimer = window.setTimeout(self.close, delay); - }, + end, hideTooltip, + { passive: true }, + ); + el.addEventListener( + "focusout", hideTooltip, { passive: true }, ); diff --git a/packages/client/src/pages/admin/_header_.vue b/packages/client/src/pages/admin/_header_.vue index 69fd1bc58..bf070e269 100644 --- a/packages/client/src/pages/admin/_header_.vue +++ b/packages/client/src/pages/admin/_header_.vue @@ -313,11 +313,7 @@ onUnmounted(() => { font-weight: normal; opacity: 0.7; - &:hover { - opacity: 1; - } - - &.active { + &:hover, &:focus-visible, &.active { opacity: 1; } diff --git a/packages/client/src/pages/admin/overview.moderators.vue b/packages/client/src/pages/admin/overview.moderators.vue index 6184cfb10..db953b890 100644 --- a/packages/client/src/pages/admin/overview.moderators.vue +++ b/packages/client/src/pages/admin/overview.moderators.vue @@ -12,7 +12,7 @@ class="user" :to="`/user-info/${user.id}`" > - <MkAvatar :user="user" class="avatar" indicator /> + <MkAvatar :user="user" class="avatar" indicator disableLink /> </MkA> </div> </Transition> diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue index 2aac52163..35279495b 100644 --- a/packages/client/src/pages/follow-requests.vue +++ b/packages/client/src/pages/follow-requests.vue @@ -23,6 +23,7 @@ class="avatar" :user="req.follower" :show-indicator="true" + disableLink /> <div class="body"> <div class="name"> diff --git a/packages/client/src/pages/settings/accounts.vue b/packages/client/src/pages/settings/accounts.vue index ec2cd2477..3010354b6 100644 --- a/packages/client/src/pages/settings/accounts.vue +++ b/packages/client/src/pages/settings/accounts.vue @@ -6,14 +6,14 @@ {{ i18n.ts.addAccount }}</FormButton > - <div + <button v-for="account in accounts" :key="account.id" class="_panel _button lcjjdxlm" @click="menu(account, $event)" > <div class="avatar"> - <MkAvatar :user="account" class="avatar" /> + <MkAvatar :user="account" class="avatar" disableLink /> </div> <div class="body"> <div class="name"> @@ -23,7 +23,7 @@ <MkAcct :user="account" /> </div> </div> - </div> + </button> </FormSuspense> </div> </template> @@ -158,6 +158,8 @@ definePageMetadata({ .lcjjdxlm { display: flex; padding: 16px; + width: 100%; + text-align: unset; > .avatar { display: block; diff --git a/packages/client/src/style.scss b/packages/client/src/style.scss index 051edf6e0..52c7b62f4 100644 --- a/packages/client/src/style.scss +++ b/packages/client/src/style.scss @@ -204,10 +204,6 @@ hr { pointer-events: none; } - &:focus-visible { - outline: none; - } - &:disabled { opacity: 0.5; cursor: default; diff --git a/packages/client/src/ui/_common_/navbar-for-mobile.vue b/packages/client/src/ui/_common_/navbar-for-mobile.vue index 43c91d147..39abb7c26 100644 --- a/packages/client/src/ui/_common_/navbar-for-mobile.vue +++ b/packages/client/src/ui/_common_/navbar-for-mobile.vue @@ -18,6 +18,7 @@ <MkAvatar :user="$i" class="icon" + disableLink /><!-- <MkAcct class="text" :user="$i"/> --> </button> </div> diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue index 380f77c3c..20c177f37 100644 --- a/packages/client/src/ui/_common_/navbar.vue +++ b/packages/client/src/ui/_common_/navbar.vue @@ -18,6 +18,7 @@ <MkAvatar :user="$i" class="icon" + disableLink /><!-- <MkAcct class="text" :user="$i"/> --> </button> </div> @@ -334,6 +335,7 @@ function more(ev: MouseEvent) { } &:hover, + &:focus-within, &.active { &:before { background: var(--accentLighten); @@ -398,8 +400,6 @@ function more(ev: MouseEvent) { padding-left: 30px; line-height: 2.85rem; margin-bottom: 0.5rem; - text-overflow: ellipsis; - overflow: hidden; white-space: nowrap; width: 100%; text-align: left; @@ -425,9 +425,12 @@ function more(ev: MouseEvent) { > .text { position: relative; font-size: 0.9em; + overflow: hidden; + text-overflow: ellipsis; } - &:hover { + &:hover, + &:focus-within { text-decoration: none; color: var(--navHoverFg); transition: all 0.4s ease; @@ -437,7 +440,8 @@ function more(ev: MouseEvent) { color: var(--navActive); } - &:hover, + &:hover, + &:focus-within, &.active { color: var(--accent); transition: all 0.4s ease; @@ -528,6 +532,7 @@ function more(ev: MouseEvent) { } &:hover, + &:focus-within, &.active { &:before { background: var(--accentLighten); @@ -613,6 +618,7 @@ function more(ev: MouseEvent) { } &:hover, + &:focus-within, &.active { text-decoration: none; color: var(--accent); @@ -642,5 +648,12 @@ function more(ev: MouseEvent) { } } } + + .item { + outline: none; + &:focus-visible:before { + outline: auto; + } + } } </style> diff --git a/packages/client/src/ui/classic.header.vue b/packages/client/src/ui/classic.header.vue index 5c3e6b702..99a0ab098 100644 --- a/packages/client/src/ui/classic.header.vue +++ b/packages/client/src/ui/classic.header.vue @@ -83,6 +83,7 @@ <MkAvatar :user="$i" class="avatar" /><MkAcct class="acct" :user="$i" + disableLink /> </button> <div class="post" @click="post"> diff --git a/packages/client/src/ui/classic.sidebar.vue b/packages/client/src/ui/classic.sidebar.vue index b70a3c984..fa72c5765 100644 --- a/packages/client/src/ui/classic.sidebar.vue +++ b/packages/client/src/ui/classic.sidebar.vue @@ -5,7 +5,7 @@ class="item _button account" @click="openAccountMenu" > - <MkAvatar :user="$i" class="avatar" /><MkAcct + <MkAvatar :user="$i" class="avatar" disableLink /><MkAcct class="text" :user="$i" /> @@ -299,6 +299,7 @@ function openInstanceMenu(ev: MouseEvent) { width: 46px; height: 46px; padding: 0; + margin-inline: 0 !important; } } @@ -372,6 +373,7 @@ function openInstanceMenu(ev: MouseEvent) { > i { width: 32px; + justify-content: center; } > i, diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue index a721ffd0b..266effd9a 100644 --- a/packages/client/src/ui/classic.vue +++ b/packages/client/src/ui/classic.vue @@ -227,6 +227,8 @@ onMounted(() => { } .gbhvwtnk { + display: flex; + justify-content: center; $ui-font-size: 1em; $widgets-hide-threshold: 1200px; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f0b4195d..60e42e11a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,12 @@ importers: '@tensorflow/tfjs': specifier: ^3.21.0 version: 3.21.0(seedrandom@3.0.5) + focus-trap: + specifier: ^7.2.0 + version: 7.2.0 + focus-trap-vue: + specifier: ^4.0.1 + version: 4.0.1(focus-trap@7.2.0)(vue@3.2.45) js-yaml: specifier: 4.1.0 version: 4.1.0 @@ -3803,14 +3809,12 @@ packages: '@vue/shared': 3.2.45 estree-walker: 2.0.2 source-map: 0.6.1 - dev: true /@vue/compiler-dom@3.2.45: resolution: {integrity: sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==} dependencies: '@vue/compiler-core': 3.2.45 '@vue/shared': 3.2.45 - dev: true /@vue/compiler-sfc@2.7.14: resolution: {integrity: sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==} @@ -3833,14 +3837,12 @@ packages: magic-string: 0.25.9 postcss: 8.4.21 source-map: 0.6.1 - dev: true /@vue/compiler-ssr@3.2.45: resolution: {integrity: sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==} dependencies: '@vue/compiler-dom': 3.2.45 '@vue/shared': 3.2.45 - dev: true /@vue/reactivity-transform@3.2.45: resolution: {integrity: sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==} @@ -3850,20 +3852,17 @@ packages: '@vue/shared': 3.2.45 estree-walker: 2.0.2 magic-string: 0.25.9 - dev: true /@vue/reactivity@3.2.45: resolution: {integrity: sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==} dependencies: '@vue/shared': 3.2.45 - dev: true /@vue/runtime-core@3.2.45: resolution: {integrity: sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==} dependencies: '@vue/reactivity': 3.2.45 '@vue/shared': 3.2.45 - dev: true /@vue/runtime-dom@3.2.45: resolution: {integrity: sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==} @@ -3871,7 +3870,6 @@ packages: '@vue/runtime-core': 3.2.45 '@vue/shared': 3.2.45 csstype: 2.6.21 - dev: true /@vue/server-renderer@3.2.45(vue@3.2.45): resolution: {integrity: sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==} @@ -3881,11 +3879,9 @@ packages: '@vue/compiler-ssr': 3.2.45 '@vue/shared': 3.2.45 vue: 3.2.45 - dev: true /@vue/shared@3.2.45: resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==} - dev: true /@webassemblyjs/ast@1.11.1: resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==} @@ -6074,7 +6070,6 @@ packages: /csstype@2.6.21: resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==} - dev: true /csstype@3.1.1: resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} @@ -6979,7 +6974,6 @@ packages: /estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - dev: true /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} @@ -7445,6 +7439,22 @@ packages: readable-stream: 2.3.7 dev: true + /focus-trap-vue@4.0.1(focus-trap@7.2.0)(vue@3.2.45): + resolution: {integrity: sha512-2iqOeoSvgq7Um6aL+255a/wXPskj6waLq2oKCa4gOnMORPo15JX7wN6J5bl1SMhMlTlkHXGSrQ9uJPJLPZDl5w==} + peerDependencies: + focus-trap: ^7.0.0 + vue: ^3.0.0 + dependencies: + focus-trap: 7.2.0 + vue: 3.2.45 + dev: false + + /focus-trap@7.2.0: + resolution: {integrity: sha512-v4wY6HDDYvzkBy4735kW5BUEuw6Yz9ABqMYLuTNbzAFPcBOGiGHwwcNVMvUz4G0kgSYh13wa/7TG3XwTeT4O/A==} + dependencies: + tabbable: 6.1.1 + dev: false + /follow-redirects@1.15.2(debug@4.3.4): resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} engines: {node: '>=4.0'} @@ -10350,7 +10360,6 @@ packages: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} dependencies: sourcemap-codec: 1.4.8 - dev: true /mailcheck@1.1.1: resolution: {integrity: sha512-3WjL8+ZDouZwKlyJBMp/4LeziLFXgleOdsYu87piGcMLqhBzCsy2QFdbtAwv757TFC/rtqd738fgJw1tFQCSgA==} @@ -13267,7 +13276,6 @@ packages: /sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} deprecated: Please use @jridgewell/sourcemap-codec instead - dev: true /sparkles@1.0.1: resolution: {integrity: sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==} @@ -13686,6 +13694,10 @@ packages: resolution: {integrity: sha512-g9rPT3V1Q4WjWFZ/t5BdGC1mT/FpYnsLdBl+M5e6MlRkuE1RSR+R43wcY/3mKI59B9KEr+vxdWCuWNMD3oNHKA==} dev: true + /tabbable@6.1.1: + resolution: {integrity: sha512-4kl5w+nCB44EVRdO0g/UGoOp3vlwgycUVtkk/7DPyeLZUCuNFFKCFG6/t/DgHLrUPHjrZg6s5tNm+56Q2B0xyg==} + dev: false + /tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} @@ -14776,7 +14788,6 @@ packages: '@vue/runtime-dom': 3.2.45 '@vue/server-renderer': 3.2.45(vue@3.2.45) '@vue/shared': 3.2.45 - dev: true /vuedraggable@4.1.0(vue@3.2.45): resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}