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>
This commit is contained in:
Free 2023-04-29 22:30:14 +00:00 committed by Kainoa Kanter
parent f3b45186fb
commit c1d5922acb
41 changed files with 601 additions and 493 deletions

View File

@ -40,6 +40,8 @@
"@bull-board/ui": "^4.10.2", "@bull-board/ui": "^4.10.2",
"@napi-rs/cli": "^2.15.0", "@napi-rs/cli": "^2.15.0",
"@tensorflow/tfjs": "^3.21.0", "@tensorflow/tfjs": "^3.21.0",
"focus-trap": "^7.2.0",
"focus-trap-vue": "^4.0.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"seedrandom": "^3.0.5" "seedrandom": "^3.0.5"
}, },

View File

@ -195,8 +195,7 @@ function onMousedown(evt: MouseEvent): void {
} }
&:focus-visible { &:focus-visible {
outline: solid 2px var(--focus); outline: auto;
outline-offset: 2px;
} }
&.inline { &.inline {

View File

@ -1,5 +1,6 @@
<template> <template>
<button <button
ref="el"
class="_button" class="_button"
:class="{ showLess: modelValue, fade: !modelValue }" :class="{ showLess: modelValue, fade: !modelValue }"
@click.stop="toggle" @click.stop="toggle"
@ -12,7 +13,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue"; import { computed, ref } from "vue";
import { length } from "stringz"; import { length } from "stringz";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import { concat } from "@/scripts/array"; import { concat } from "@/scripts/array";
@ -27,6 +28,8 @@ const emit = defineEmits<{
(ev: "update:modelValue", v: boolean): void; (ev: "update:modelValue", v: boolean): void;
}>(); }>();
const el = ref<HTMLElement>();
const label = computed(() => { const label = computed(() => {
return concat([ return concat([
props.note.text props.note.text
@ -43,6 +46,14 @@ const label = computed(() => {
const toggle = () => { const toggle = () => {
emit("update:modelValue", !props.modelValue); emit("update:modelValue", !props.modelValue);
}; };
function focus() {
el.value.focus();
}
defineExpose({
focus
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -62,7 +73,7 @@ const toggle = () => {
} }
} }
} }
&:hover > span { &:hover > span, &:focus > span {
background: var(--cwFg) !important; background: var(--cwFg) !important;
color: var(--cwBg) !important; color: var(--cwBg) !important;
} }
@ -73,6 +84,7 @@ const toggle = () => {
bottom: 0; bottom: 0;
left: 0; left: 0;
width: 100%; width: 100%;
z-index: 2;
> span { > span {
display: inline-block; display: inline-block;
background: var(--panel); background: var(--panel);
@ -81,7 +93,7 @@ const toggle = () => {
border-radius: 999px; border-radius: 999px;
box-shadow: 0 2px 6px rgb(0 0 0 / 20%); box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
} }
&:hover { &:hover, &:focus {
> span { > span {
background: var(--panelHighlight); background: var(--panelHighlight);
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div ref="thumbnail" class="zdjebgpv"> <button ref="thumbnail" class="zdjebgpv">
<ImgWithBlurhash <ImgWithBlurhash
v-if="isThumbnailAvailable" v-if="isThumbnailAvailable"
:hash="file.blurhash" :hash="file.blurhash"
@ -36,7 +36,7 @@
v-if="isThumbnailAvailable && is === 'video'" v-if="isThumbnailAvailable && is === 'video'"
class="ph-file-video ph-bold ph-lg icon-sub" class="ph-file-video ph-bold ph-lg icon-sub"
></i> ></i>
</div> </button>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -88,6 +88,9 @@ const isThumbnailAvailable = computed(() => {
background: var(--panel); background: var(--panel);
border-radius: 8px; border-radius: 8px;
overflow: clip; overflow: clip;
border: 0;
padding: 0;
cursor: pointer;
> .icon-sub { > .icon-sub {
position: absolute; position: absolute;

View File

@ -1,157 +1,160 @@
<template> <template>
<div <FocusTrap v-bind:active="isActive">
class="omfetrab" <div
:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" class="omfetrab"
:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
> :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
<input tabindex="-1"
ref="search" >
v-model.trim="q" <input
class="search" ref="search"
data-prevent-emoji-insert v-model.trim="q"
:class="{ filled: q != null && q != '' }" class="search"
:placeholder="i18n.ts.search" data-prevent-emoji-insert
type="search" :class="{ filled: q != null && q != '' }"
@paste.stop="paste" :placeholder="i18n.ts.search"
@keyup.enter="done()" type="search"
/> @paste.stop="paste"
<div ref="emojis" class="emojis"> @keyup.enter="done()"
<section class="result"> />
<div v-if="searchResultCustom.length > 0" class="body"> <div ref="emojis" class="emojis">
<button <section class="result">
v-for="emoji in searchResultCustom" <div v-if="searchResultCustom.length > 0" class="body">
: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">
<button <button
v-for="emoji in pinned" v-for="emoji in searchResultCustom"
:key="emoji" :key="emoji.id"
class="_button item" class="_button item"
:title="emoji.name"
tabindex="0" tabindex="0"
@click="chosen(emoji, $event)" @click="chosen(emoji, $event)"
> >
<MkEmoji <!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
<img
class="emoji" class="emoji"
:emoji="emoji" :src="
:normal="true" disableShowingAnimatedImages
? getStaticImageUrl(emoji.url)
: emoji.url
"
/> />
</button> </button>
</div> </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>
<section> <div v-if="tab === 'index'" class="group index">
<header class="_acrylic"> <section v-if="showPinned">
<i class="ph-alarm ph-bold ph-fw ph-lg"></i> <div class="body">
{{ i18n.ts.recentUsed }} <button
</header> v-for="emoji in pinned"
<div class="body"> :key="emoji"
<button class="_button item"
v-for="emoji in recentlyUsedEmojis" tabindex="0"
:key="emoji" @click="chosen(emoji, $event)"
class="_button item" >
@click="chosen(emoji, $event)" <MkEmoji
> class="emoji"
<MkEmoji :emoji="emoji"
class="emoji" :normal="true"
:emoji="emoji" />
:normal="true" </button>
/> </div>
</button> </section>
</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>
<div v-once class="group"> <div class="tabs">
<header>{{ i18n.ts.customEmojis }}</header> <button
<XSection class="_button tab"
v-for="category in customEmojiCategories" :class="{ active: tab === 'index' }"
:key="'custom:' + category" @click="tab = 'index'"
:initial-shown="false"
:emojis="
customEmojis
.filter((e) => e.category === category)
.map((e) => ':' + e.name + ':')
"
@chosen="chosen"
>{{ category || i18n.ts.other }}</XSection
> >
</div> <i class="ph-asterisk ph-bold ph-lg ph-fw ph-lg"></i>
<div v-once class="group"> </button>
<header>{{ i18n.ts.emoji }}</header> <button
<XSection class="_button tab"
v-for="category in categories" :class="{ active: tab === 'custom' }"
:key="category" @click="tab = 'custom'"
:emojis="
emojilist
.filter((e) => e.category === category)
.map((e) => e.char)
"
@chosen="chosen"
>{{ category }}</XSection
> >
<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> </div>
<div class="tabs"> </FocusTrap>
<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>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -171,6 +174,7 @@ import { deviceKind } from "@/scripts/device-kind";
import { emojiCategories, instance } from "@/instance"; import { emojiCategories, instance } from "@/instance";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { FocusTrap } from 'focus-trap-vue';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{

View File

@ -139,7 +139,7 @@ function close() {
height: 100px; height: 100px;
border-radius: 10px; border-radius: 10px;
&:hover { &:hover, &:focus-visible {
color: var(--accent); color: var(--accent);
background: var(--accentedBg); background: var(--accentedBg);
text-decoration: none; text-decoration: none;

View File

@ -138,6 +138,10 @@ watch(
background-position: center; background-position: center;
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
box-sizing: border-box;
&:focus-visible {
border: 2px solid var(--accent);
}
> .gif { > .gif {
background-color: var(--fg); background-color: var(--fg);

View File

@ -1,14 +1,14 @@
<template> <template>
<div ref="el" class="sfhdhdhr"> <div ref="el" class="sfhdhdhr" tabindex="-1">
<MkMenu <MkMenu
ref="menu" ref="menu"
:items="items" :items="items"
:align="align" :align="align"
:width="width" :width="width"
:as-drawer="false" :as-drawer="false"
@close="onChildClosed" @close="onChildClosed"
/> />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -23,7 +23,6 @@ import {
} from "vue"; } from "vue";
import MkMenu from "./MkMenu.vue"; import MkMenu from "./MkMenu.vue";
import { MenuItem } from "@/types/menu"; import { MenuItem } from "@/types/menu";
import * as os from "@/os";
const props = defineProps<{ const props = defineProps<{
items: MenuItem[]; items: MenuItem[];

View File

@ -1,191 +1,188 @@
<template> <template>
<div> <FocusTrap v-bind:active="isActive">
<div <div tabindex="-1" v-focus>
ref="itemsEl" <div
v-hotkey="keymap" ref="itemsEl"
class="rrevdjwt _popup _shadow" class="rrevdjwt _popup _shadow"
:class="{ center: align === 'center', asDrawer }" :class="{ center: align === 'center', asDrawer }"
:style="{ :style="{
width: width && !asDrawer ? width + 'px' : '', width: width && !asDrawer ? width + 'px' : '',
maxHeight: maxHeight ? maxHeight + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '',
}" }"
@contextmenu.self="(e) => e.preventDefault()" @contextmenu.self="(e) => e.preventDefault()"
> >
<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>
<span v-else-if="item.type === 'label'" class="label item"> <span v-else-if="item.type === 'label'" class="label item">
<span :style="item.textStyle || ''">{{ item.text }}</span> <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>
</span> </span>
<MkAvatar <span
v-if="item.avatar" v-else-if="item.type === 'pending'"
:user="item.avatar" class="pending item"
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><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> </span>
<button </div>
v-else-if="item.type === 'parent'" <div v-if="childMenu" class="child">
:tabindex="i" <XChild
class="_button item parent" ref="child"
:class="{ childShowing: childShowingItem === item }" :items="childMenu"
@mouseenter="showChildren(item, $event)" :target-element="childTarget"
> :root-element="itemsEl"
<i showing
v-if="item.icon" @actioned="childActioned"
class="ph-fw ph-lg" />
:class="item.icon" </div>
></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>
<div v-if="childMenu" class="child"> </FocusTrap>
<XChild
ref="child"
:items="childMenu"
:target-element="childTarget"
:root-element="itemsEl"
showing
@actioned="childActioned"
/>
</div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -206,6 +203,7 @@ import FormSwitch from "@/components/form/switch.vue";
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from "@/types/menu"; import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from "@/types/menu";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { FocusTrap } from 'focus-trap-vue';
const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue")); const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue"));
@ -228,12 +226,6 @@ let items2: InnerMenuItem[] = $ref([]);
let child = $ref<InstanceType<typeof XChild>>(); 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>(); let childShowingItem = $ref<MenuItem | null>();
watch( watch(
@ -364,8 +356,7 @@ onBeforeUnmount(() => {
font-size: 0.9em; font-size: 0.9em;
line-height: 20px; line-height: 20px;
text-align: left; text-align: left;
overflow: hidden; outline: none;
text-overflow: ellipsis;
&:before { &:before {
content: ""; content: "";
@ -389,7 +380,7 @@ onBeforeUnmount(() => {
transform: translateY(0em); transform: translateY(0em);
} }
&:not(:disabled):hover { &:not(:disabled):hover, &:focus-visible {
color: var(--accent); color: var(--accent);
text-decoration: none; text-decoration: none;
@ -397,6 +388,9 @@ onBeforeUnmount(() => {
background: var(--accentedBg); background: var(--accentedBg);
} }
} }
&:focus-visible:before {
outline: auto;
}
&.danger { &.danger {
color: #eb6f92; color: #eb6f92;

View File

@ -14,54 +14,59 @@
: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"
> >
<div <FocusTrap v-model:active="isActive">
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,
}"
>
<div <div
class="_modalBg data-cy-bg" v-show="manualShowing != null ? manualShowing : showing"
v-hotkey.global="keymap"
:class="[ :class="[
$style.bg, $style.root,
{ {
[$style.bgTransparent]: isEnableBgTransparent, [$style.drawer]: type === 'drawer',
'data-cy-transparent': isEnableBgTransparent, [$style.dialog]: type === 'dialog' || type === 'dialog:top',
[$style.popup]: type === 'popup',
}, },
]" ]"
:style="{ zIndex }" :style="{
@click="onBgClick" zIndex,
@mousedown="onBgClick" pointerEvents: (manualShowing != null ? manualShowing : showing)
@contextmenu.prevent.stop="() => {}" ? 'auto'
></div> : 'none',
<div '--transformOrigin': transformOrigin,
ref="content" }"
:class="[ tabindex="-1"
$style.content, v-focus
{ [$style.fixed]: fixed, top: type === 'dialog:top' },
]"
:style="{ zIndex }"
@click.self="onBgClick"
> >
<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>
</div> </FocusTrap>
</Transition> </Transition>
</template> </template>
@ -71,6 +76,7 @@ import * as os from "@/os";
import { isTouchUsing } from "@/scripts/touch"; import { isTouchUsing } from "@/scripts/touch";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { deviceKind } from "@/scripts/device-kind"; import { deviceKind } from "@/scripts/device-kind";
import { FocusTrap } from 'focus-trap-vue';
function getFixedContainer(el: Element | null): Element | null { function getFixedContainer(el: Element | null): Element | null {
if (el == null || el.tagName === "BODY") return null; if (el == null || el.tagName === "BODY") return null;
@ -166,6 +172,7 @@ let transitionDuration = $computed(() =>
let contentClicking = false; let contentClicking = false;
const focusedElement = document.activeElement;
function close(opts: { useSendAnimation?: boolean } = {}) { function close(opts: { useSendAnimation?: boolean } = {}) {
if (opts.useSendAnimation) { if (opts.useSendAnimation) {
useSendAnime = true; useSendAnime = true;
@ -175,10 +182,12 @@ function close(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");
focusedElement.focus();
} }
function onBgClick() { function onBgClick() {
if (contentClicking) return; if (contentClicking) return;
focusedElement.focus();
emit("click"); emit("click");
} }
@ -481,6 +490,7 @@ defineExpose({
} }
.root { .root {
outline: none;
&.dialog { &.dialog {
> .content { > .content {
position: fixed; position: fixed;

View File

@ -158,6 +158,7 @@ function onContextmenu(ev: MouseEvent) {
flex-direction: column; flex-direction: column;
contain: content; contain: content;
border-radius: var(--radius); border-radius: var(--radius);
margin: auto;
--root-margin: 24px; --root-margin: 24px;

View File

@ -3,59 +3,64 @@
ref="modal" ref="modal"
:prefer-type="'dialog'" :prefer-type="'dialog'"
@click="onBgClick" @click="onBgClick"
@keyup.esc="$emit('close')"
@closed="$emit('closed')" @closed="$emit('closed')"
> >
<div <FocusTrap v-model:active="isActive">
ref="rootEl" <div
class="ebkgoccj" ref="rootEl"
:style="{ class="ebkgoccj"
width: `${width}px`, :style="{
height: scroll width: `${width}px`,
? height height: scroll
? `${height}px` ? height
: null ? `${height}px`
: height : null
? `min(${height}px, 100%)` : height
: '100%', ? `min(${height}px, 100%)`
}" : '100%',
@keydown="onKeydown" }"
> @keydown="onKeydown"
<div ref="headerEl" class="header"> tabindex="-1"
<button >
v-if="withOkButton" <div ref="headerEl" class="header">
class="_button" <button
@click="$emit('close')" v-if="withOkButton"
> class="_button"
<i class="ph-x ph-bold ph-lg"></i> @click="$emit('close')"
</button> >
<span class="title"> <i class="ph-x ph-bold ph-lg"></i>
<slot name="header"></slot> </button>
</span> <span class="title">
<button <slot name="header"></slot>
v-if="!withOkButton" </span>
class="_button" <button
@click="$emit('close')" v-if="!withOkButton"
> class="_button"
<i class="ph-x ph-bold ph-lg"></i> @click="$emit('close')"
</button> >
<button <i class="ph-x ph-bold ph-lg"></i>
v-if="withOkButton" </button>
class="_button" <button
:disabled="okButtonDisabled" v-if="withOkButton"
@click="$emit('ok')" class="_button"
> :disabled="okButtonDisabled"
<i class="ph-check ph-bold ph-lg"></i> @click="$emit('ok')"
</button> >
<i class="ph-check ph-bold ph-lg"></i>
</button>
</div>
<div class="body">
<slot :width="bodyWidth" :height="bodyHeight"></slot>
</div>
</div> </div>
<div class="body"> </FocusTrap>
<slot :width="bodyWidth" :height="bodyHeight"></slot>
</div>
</div>
</MkModal> </MkModal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted } from "vue"; import { onMounted, onUnmounted } from "vue";
import { FocusTrap } from 'focus-trap-vue';
import MkModal from "./MkModal.vue"; import MkModal from "./MkModal.vue";
const props = withDefaults( const props = withDefaults(

View File

@ -84,6 +84,7 @@
:detailedView="detailedView" :detailedView="detailedView"
:parentId="appearNote.parentId" :parentId="appearNote.parentId"
@push="(e) => router.push(notePage(e))" @push="(e) => router.push(notePage(e))"
@focusfooter="footerEl.focus()"
></MkSubNoteContent> ></MkSubNoteContent>
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini /> <MkLoading v-if="translating" mini />
@ -117,7 +118,7 @@
<MkTime :time="appearNote.createdAt" mode="absolute" /> <MkTime :time="appearNote.createdAt" mode="absolute" />
</MkA> </MkA>
</div> </div>
<footer ref="el" class="footer" @click.stop> <footer ref="footerEl" class="footer" @click.stop tabindex="-1">
<XReactionsViewer <XReactionsViewer
v-if="enableEmojiReactions" v-if="enableEmojiReactions"
ref="reactionsViewer" ref="reactionsViewer"
@ -278,6 +279,7 @@ const isRenote =
note.poll == null; note.poll == null;
const el = ref<HTMLElement>(); const el = ref<HTMLElement>();
const footerEl = ref<HTMLElement>();
const menuButton = ref<HTMLElement>(); const menuButton = ref<HTMLElement>();
const starButton = ref<InstanceType<typeof XStarButton>>(); const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
@ -298,8 +300,8 @@ const keymap = {
r: () => reply(true), r: () => reply(true),
"e|a|plus": () => react(true), "e|a|plus": () => react(true),
q: () => renoteButton.value.renote(true), q: () => renoteButton.value.renote(true),
"up|k|shift+tab": focusBefore, "up|k": focusBefore,
"down|j|tab": focusAfter, "down|j": focusAfter,
esc: blur, esc: blur,
"m|o": () => menu(true), "m|o": () => menu(true),
s: () => showContent.value !== showContent.value, s: () => showContent.value !== showContent.value,

View File

@ -1,6 +1,6 @@
<template> <template>
<div v-size="{ min: [350, 500] }" class="fefdfafb"> <div v-size="{ min: [350, 500] }" class="fefdfafb">
<MkAvatar class="avatar" :user="$i" /> <MkAvatar class="avatar" :user="$i" disableLink />
<div class="main"> <div class="main">
<div class="header"> <div class="header">
<MkUserName :user="$i" /> <MkUserName :user="$i" />

View File

@ -26,6 +26,7 @@
:note="note" :note="note"
:parentId="appearNote.parentId" :parentId="appearNote.parentId"
:conversation="conversation" :conversation="conversation"
@focusfooter="footerEl.focus()"
/> />
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini /> <MkLoading v-if="translating" mini />
@ -46,7 +47,7 @@
</div> </div>
</div> </div>
</div> </div>
<footer class="footer" @click.stop> <footer ref="footerEl" class="footer" @click.stop tabindex="-1">
<XReactionsViewer <XReactionsViewer
v-if="enableEmojiReactions" v-if="enableEmojiReactions"
ref="reactionsViewer" ref="reactionsViewer"
@ -212,6 +213,7 @@ const isRenote =
note.poll == null; note.poll == null;
const el = ref<HTMLElement>(); const el = ref<HTMLElement>();
const footerEl = ref<HTMLElement>();
const menuButton = ref<HTMLElement>(); const menuButton = ref<HTMLElement>();
const starButton = ref<InstanceType<typeof XStarButton>>(); const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); const renoteButton = ref<InstanceType<typeof XRenoteButton>>();

View File

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

View File

@ -198,7 +198,6 @@ export default defineComponent({
height: 64px; height: 64px;
margin-right: 4px; margin-right: 4px;
border-radius: 4px; border-radius: 4px;
overflow: hidden;
cursor: move; cursor: move;
&:hover > .remove { &:hover > .remove {

View File

@ -35,7 +35,11 @@
class="content" class="content"
:class="{ collapsed, isLong, showContent: note.cw && !showContent }" :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" <span v-if="note.deletedAt" style="opacity: 0.5"
>({{ i18n.ts.deleted }})</span >({{ i18n.ts.deleted }})</span
> >
@ -96,15 +100,20 @@
<XNoteSimple :note="note.renote" /> <XNoteSimple :note="note.renote" />
</div> </div>
</template> </template>
<div
v-if="note.cw && !showContent"
tabindex="0"
v-on:focus="cwButton?.focus()"
></div>
</div> </div>
<XShowMoreButton v-if="isLong" v-model="collapsed"></XShowMoreButton> <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>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {} from "vue"; import { ref } from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import * as mfm from "mfm-js"; import * as mfm from "mfm-js";
import XNoteSimple from "@/components/MkNoteSimple.vue"; import XNoteSimple from "@/components/MkNoteSimple.vue";
@ -126,8 +135,10 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "push", v): void; (ev: "push", v): void;
(ev: "focusfooter"): void;
}>(); }>();
const cwButton = ref<HTMLElement>();
const isLong = const isLong =
!props.detailedView && !props.detailedView &&
props.note.cw == null && props.note.cw == null &&
@ -140,6 +151,13 @@ const urls = props.note.text
: null; : null;
let showContent = $ref(false); let showContent = $ref(false);
function focusFooter(ev) {
if (ev.key == "Tab" && !ev.getModifierState("Shift")) {
emit("focusfooter");
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -231,6 +249,9 @@ let showContent = $ref(false);
margin-top: -50px; margin-top: -50px;
padding-top: 50px; padding-top: 50px;
overflow: hidden; overflow: hidden;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
} }
&.collapsed > .body { &.collapsed > .body {
box-sizing: border-box; box-sizing: border-box;

View File

@ -9,7 +9,6 @@
v-if="item.type === 'a'" v-if="item.type === 'a'"
:href="item.href" :href="item.href"
:target="item.target" :target="item.target"
:tabindex="i"
class="_button item" class="_button item"
:class="{ danger: item.danger, active: item.active }" :class="{ danger: item.danger, active: item.active }"
> >
@ -22,7 +21,6 @@
</a> </a>
<button <button
v-else-if="item.type === 'button'" v-else-if="item.type === 'button'"
:tabindex="i"
class="_button item" class="_button item"
:class="{ danger: item.danger, active: item.active }" :class="{ danger: item.danger, active: item.active }"
:disabled="item.active" :disabled="item.active"
@ -38,7 +36,6 @@
<MkA <MkA
v-else v-else
:to="item.to" :to="item.to"
:tabindex="i"
class="_button item" class="_button item"
:class="{ danger: item.danger, active: item.active }" :class="{ danger: item.danger, active: item.active }"
> >
@ -99,7 +96,7 @@ export default defineComponent({
font-size: 0.9em; font-size: 0.9em;
margin-bottom: 0.3rem; margin-bottom: 0.3rem;
&:hover { &:hover, &:focus-visible {
text-decoration: none; text-decoration: none;
background: var(--panelHighlight); background: var(--panelHighlight);
} }

View File

@ -46,6 +46,7 @@
:user="user" :user="user"
class="avatar" class="avatar"
:show-indicator="true" :show-indicator="true"
disableLink
/> />
<div class="body"> <div class="body">
<MkUserName :user="user" class="name" /> <MkUserName :user="user" class="name" />
@ -73,6 +74,7 @@
:user="user" :user="user"
class="avatar" class="avatar"
:show-indicator="true" :show-indicator="true"
disableLink
/> />
<div class="body"> <div class="body">
<MkUserName :user="user" class="name" /> <MkUserName :user="user" class="name" />

View File

@ -7,7 +7,7 @@
> >
<div class="beaffaef"> <div class="beaffaef">
<div v-for="u in users" :key="u.id" class="user"> <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" /> <MkUserName class="name" :user="u" :nowrap="true" />
</div> </div>
<div v-if="users.length < count" class="omitted"> <div v-if="users.length < count" class="omitted">

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="vjoppmmu"> <div class="vjoppmmu">
<template v-if="edit"> <template v-if="edit">
<header> <header tabindex="-1" v-focus>
<MkSelect <MkSelect
v-model="widgetAdderSelected" v-model="widgetAdderSelected"
style="margin-bottom: var(--margin)" style="margin-bottom: var(--margin)"

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="dwzlatin" :class="{ opened }"> <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="icon"><slot name="icon"></slot></span>
<span class="text"><slot name="label"></slot></span> <span class="text"><slot name="label"></slot></span>
<span class="right"> <span class="right">
@ -8,7 +8,7 @@
<i v-if="opened" class="ph-caret-up ph-bold ph-lg icon"></i> <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> <i v-else class="ph-caret-down ph-bold ph-lg icon"></i>
</span> </span>
</div> </button>
<KeepAlive> <KeepAlive>
<div v-if="openedAtLeastOnce" v-show="opened" class="body"> <div v-if="openedAtLeastOnce" v-show="opened" class="body">
<MkSpacer :margin-min="14" :margin-max="22"> <MkSpacer :margin-min="14" :margin-max="22">

View File

@ -66,6 +66,9 @@ function toggle(): void {
&:hover { &:hover {
border-color: var(--inputBorderHover) !important; border-color: var(--inputBorderHover) !important;
} }
&:focus-within {
outline: auto;
}
&.checked { &.checked {
background-color: var(--accentedBg) !important; background-color: var(--accentedBg) !important;

View File

@ -99,6 +99,9 @@ const toggle = () => {
border-color: var(--inputBorderHover) !important; border-color: var(--inputBorderHover) !important;
} }
} }
&:focus-within > .button {
outline: auto;
}
> .label { > .label {
margin-left: 12px; margin-left: 12px;

View File

@ -19,6 +19,7 @@
class="avatar" class="avatar"
:user="$i" :user="$i"
:disable-preview="true" :disable-preview="true"
disableLink
/> />
</div> </div>
<template v-if="metadata"> <template v-if="metadata">
@ -33,6 +34,7 @@
:user="metadata.avatar" :user="metadata.avatar"
:disable-preview="true" :disable-preview="true"
:show-indicator="true" :show-indicator="true"
disableLink
/> />
<i <i
v-else-if="metadata.icon && !narrow" v-else-if="metadata.icon && !narrow"

View File

@ -5,6 +5,9 @@
:is="currentPageComponent" :is="currentPageComponent"
:key="key" :key="key"
v-bind="Object.fromEntries(currentPageProps)" v-bind="Object.fromEntries(currentPageProps)"
tabindex="-1"
v-focus
style="outline: none;"
/> />
<template #fallback> <template #fallback>

View File

@ -0,0 +1,3 @@
export default {
mounted: (el) => el.focus()
}

View File

@ -11,6 +11,7 @@ import anim from "./anim";
import clickAnime from "./click-anime"; import clickAnime from "./click-anime";
import panel from "./panel"; import panel from "./panel";
import adaptiveBorder from "./adaptive-border"; import adaptiveBorder from "./adaptive-border";
import focus from "./focus";
export default function (app: App) { export default function (app: App) {
app.directive("userPreview", userPreview); app.directive("userPreview", userPreview);
@ -25,4 +26,5 @@ export default function (app: App) {
app.directive("click-anime", clickAnime); app.directive("click-anime", clickAnime);
app.directive("panel", panel); app.directive("panel", panel);
app.directive("adaptive-border", adaptiveBorder); app.directive("adaptive-border", adaptiveBorder);
app.directive("focus", focus);
} }

View File

@ -76,23 +76,32 @@ export default {
ev.preventDefault(); 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( el.addEventListener(
start, start, showTooltip,
() => { { passive: true },
window.clearTimeout(self.showTimer); );
window.clearTimeout(self.hideTimer); el.addEventListener(
self.showTimer = window.setTimeout(self.show, delay); "focusin", showTooltip,
},
{ passive: true }, { passive: true },
); );
el.addEventListener( el.addEventListener(
end, end, hideTooltip,
() => { { passive: true },
window.clearTimeout(self.showTimer); );
window.clearTimeout(self.hideTimer); el.addEventListener(
self.hideTimer = window.setTimeout(self.close, delay); "focusout", hideTooltip,
},
{ passive: true }, { passive: true },
); );

View File

@ -313,11 +313,7 @@ onUnmounted(() => {
font-weight: normal; font-weight: normal;
opacity: 0.7; opacity: 0.7;
&:hover { &:hover, &:focus-visible, &.active {
opacity: 1;
}
&.active {
opacity: 1; opacity: 1;
} }

View File

@ -12,7 +12,7 @@
class="user" class="user"
:to="`/user-info/${user.id}`" :to="`/user-info/${user.id}`"
> >
<MkAvatar :user="user" class="avatar" indicator /> <MkAvatar :user="user" class="avatar" indicator disableLink />
</MkA> </MkA>
</div> </div>
</Transition> </Transition>

View File

@ -23,6 +23,7 @@
class="avatar" class="avatar"
:user="req.follower" :user="req.follower"
:show-indicator="true" :show-indicator="true"
disableLink
/> />
<div class="body"> <div class="body">
<div class="name"> <div class="name">

View File

@ -6,14 +6,14 @@
{{ i18n.ts.addAccount }}</FormButton {{ i18n.ts.addAccount }}</FormButton
> >
<div <button
v-for="account in accounts" v-for="account in accounts"
:key="account.id" :key="account.id"
class="_panel _button lcjjdxlm" class="_panel _button lcjjdxlm"
@click="menu(account, $event)" @click="menu(account, $event)"
> >
<div class="avatar"> <div class="avatar">
<MkAvatar :user="account" class="avatar" /> <MkAvatar :user="account" class="avatar" disableLink />
</div> </div>
<div class="body"> <div class="body">
<div class="name"> <div class="name">
@ -23,7 +23,7 @@
<MkAcct :user="account" /> <MkAcct :user="account" />
</div> </div>
</div> </div>
</div> </button>
</FormSuspense> </FormSuspense>
</div> </div>
</template> </template>
@ -158,6 +158,8 @@ definePageMetadata({
.lcjjdxlm { .lcjjdxlm {
display: flex; display: flex;
padding: 16px; padding: 16px;
width: 100%;
text-align: unset;
> .avatar { > .avatar {
display: block; display: block;

View File

@ -204,10 +204,6 @@ hr {
pointer-events: none; pointer-events: none;
} }
&:focus-visible {
outline: none;
}
&:disabled { &:disabled {
opacity: 0.5; opacity: 0.5;
cursor: default; cursor: default;

View File

@ -18,6 +18,7 @@
<MkAvatar <MkAvatar
:user="$i" :user="$i"
class="icon" class="icon"
disableLink
/><!-- <MkAcct class="text" :user="$i"/> --> /><!-- <MkAcct class="text" :user="$i"/> -->
</button> </button>
</div> </div>

View File

@ -18,6 +18,7 @@
<MkAvatar <MkAvatar
:user="$i" :user="$i"
class="icon" class="icon"
disableLink
/><!-- <MkAcct class="text" :user="$i"/> --> /><!-- <MkAcct class="text" :user="$i"/> -->
</button> </button>
</div> </div>
@ -334,6 +335,7 @@ function more(ev: MouseEvent) {
} }
&:hover, &:hover,
&:focus-within,
&.active { &.active {
&:before { &:before {
background: var(--accentLighten); background: var(--accentLighten);
@ -398,8 +400,6 @@ function more(ev: MouseEvent) {
padding-left: 30px; padding-left: 30px;
line-height: 2.85rem; line-height: 2.85rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap; white-space: nowrap;
width: 100%; width: 100%;
text-align: left; text-align: left;
@ -425,9 +425,12 @@ function more(ev: MouseEvent) {
> .text { > .text {
position: relative; position: relative;
font-size: 0.9em; font-size: 0.9em;
overflow: hidden;
text-overflow: ellipsis;
} }
&:hover { &:hover,
&:focus-within {
text-decoration: none; text-decoration: none;
color: var(--navHoverFg); color: var(--navHoverFg);
transition: all 0.4s ease; transition: all 0.4s ease;
@ -437,7 +440,8 @@ function more(ev: MouseEvent) {
color: var(--navActive); color: var(--navActive);
} }
&:hover, &:hover,
&:focus-within,
&.active { &.active {
color: var(--accent); color: var(--accent);
transition: all 0.4s ease; transition: all 0.4s ease;
@ -528,6 +532,7 @@ function more(ev: MouseEvent) {
} }
&:hover, &:hover,
&:focus-within,
&.active { &.active {
&:before { &:before {
background: var(--accentLighten); background: var(--accentLighten);
@ -613,6 +618,7 @@ function more(ev: MouseEvent) {
} }
&:hover, &:hover,
&:focus-within,
&.active { &.active {
text-decoration: none; text-decoration: none;
color: var(--accent); color: var(--accent);
@ -642,5 +648,12 @@ function more(ev: MouseEvent) {
} }
} }
} }
.item {
outline: none;
&:focus-visible:before {
outline: auto;
}
}
} }
</style> </style>

View File

@ -83,6 +83,7 @@
<MkAvatar :user="$i" class="avatar" /><MkAcct <MkAvatar :user="$i" class="avatar" /><MkAcct
class="acct" class="acct"
:user="$i" :user="$i"
disableLink
/> />
</button> </button>
<div class="post" @click="post"> <div class="post" @click="post">

View File

@ -5,7 +5,7 @@
class="item _button account" class="item _button account"
@click="openAccountMenu" @click="openAccountMenu"
> >
<MkAvatar :user="$i" class="avatar" /><MkAcct <MkAvatar :user="$i" class="avatar" disableLink /><MkAcct
class="text" class="text"
:user="$i" :user="$i"
/> />
@ -299,6 +299,7 @@ function openInstanceMenu(ev: MouseEvent) {
width: 46px; width: 46px;
height: 46px; height: 46px;
padding: 0; padding: 0;
margin-inline: 0 !important;
} }
} }
@ -372,6 +373,7 @@ function openInstanceMenu(ev: MouseEvent) {
> i { > i {
width: 32px; width: 32px;
justify-content: center;
} }
> i, > i,

View File

@ -227,6 +227,8 @@ onMounted(() => {
} }
.gbhvwtnk { .gbhvwtnk {
display: flex;
justify-content: center;
$ui-font-size: 1em; $ui-font-size: 1em;
$widgets-hide-threshold: 1200px; $widgets-hide-threshold: 1200px;

View File

@ -19,6 +19,12 @@ importers:
'@tensorflow/tfjs': '@tensorflow/tfjs':
specifier: ^3.21.0 specifier: ^3.21.0
version: 3.21.0(seedrandom@3.0.5) 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: js-yaml:
specifier: 4.1.0 specifier: 4.1.0
version: 4.1.0 version: 4.1.0
@ -3803,14 +3809,12 @@ packages:
'@vue/shared': 3.2.45 '@vue/shared': 3.2.45
estree-walker: 2.0.2 estree-walker: 2.0.2
source-map: 0.6.1 source-map: 0.6.1
dev: true
/@vue/compiler-dom@3.2.45: /@vue/compiler-dom@3.2.45:
resolution: {integrity: sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==} resolution: {integrity: sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==}
dependencies: dependencies:
'@vue/compiler-core': 3.2.45 '@vue/compiler-core': 3.2.45
'@vue/shared': 3.2.45 '@vue/shared': 3.2.45
dev: true
/@vue/compiler-sfc@2.7.14: /@vue/compiler-sfc@2.7.14:
resolution: {integrity: sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==} resolution: {integrity: sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==}
@ -3833,14 +3837,12 @@ packages:
magic-string: 0.25.9 magic-string: 0.25.9
postcss: 8.4.21 postcss: 8.4.21
source-map: 0.6.1 source-map: 0.6.1
dev: true
/@vue/compiler-ssr@3.2.45: /@vue/compiler-ssr@3.2.45:
resolution: {integrity: sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==} resolution: {integrity: sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==}
dependencies: dependencies:
'@vue/compiler-dom': 3.2.45 '@vue/compiler-dom': 3.2.45
'@vue/shared': 3.2.45 '@vue/shared': 3.2.45
dev: true
/@vue/reactivity-transform@3.2.45: /@vue/reactivity-transform@3.2.45:
resolution: {integrity: sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==} resolution: {integrity: sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==}
@ -3850,20 +3852,17 @@ packages:
'@vue/shared': 3.2.45 '@vue/shared': 3.2.45
estree-walker: 2.0.2 estree-walker: 2.0.2
magic-string: 0.25.9 magic-string: 0.25.9
dev: true
/@vue/reactivity@3.2.45: /@vue/reactivity@3.2.45:
resolution: {integrity: sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==} resolution: {integrity: sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==}
dependencies: dependencies:
'@vue/shared': 3.2.45 '@vue/shared': 3.2.45
dev: true
/@vue/runtime-core@3.2.45: /@vue/runtime-core@3.2.45:
resolution: {integrity: sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==} resolution: {integrity: sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==}
dependencies: dependencies:
'@vue/reactivity': 3.2.45 '@vue/reactivity': 3.2.45
'@vue/shared': 3.2.45 '@vue/shared': 3.2.45
dev: true
/@vue/runtime-dom@3.2.45: /@vue/runtime-dom@3.2.45:
resolution: {integrity: sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==} resolution: {integrity: sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==}
@ -3871,7 +3870,6 @@ packages:
'@vue/runtime-core': 3.2.45 '@vue/runtime-core': 3.2.45
'@vue/shared': 3.2.45 '@vue/shared': 3.2.45
csstype: 2.6.21 csstype: 2.6.21
dev: true
/@vue/server-renderer@3.2.45(vue@3.2.45): /@vue/server-renderer@3.2.45(vue@3.2.45):
resolution: {integrity: sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==} resolution: {integrity: sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==}
@ -3881,11 +3879,9 @@ packages:
'@vue/compiler-ssr': 3.2.45 '@vue/compiler-ssr': 3.2.45
'@vue/shared': 3.2.45 '@vue/shared': 3.2.45
vue: 3.2.45 vue: 3.2.45
dev: true
/@vue/shared@3.2.45: /@vue/shared@3.2.45:
resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==} resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==}
dev: true
/@webassemblyjs/ast@1.11.1: /@webassemblyjs/ast@1.11.1:
resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==} resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==}
@ -6074,7 +6070,6 @@ packages:
/csstype@2.6.21: /csstype@2.6.21:
resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==} resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==}
dev: true
/csstype@3.1.1: /csstype@3.1.1:
resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
@ -6979,7 +6974,6 @@ packages:
/estree-walker@2.0.2: /estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
dev: true
/esutils@2.0.3: /esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
@ -7445,6 +7439,22 @@ packages:
readable-stream: 2.3.7 readable-stream: 2.3.7
dev: true 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): /follow-redirects@1.15.2(debug@4.3.4):
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
@ -10350,7 +10360,6 @@ packages:
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
dependencies: dependencies:
sourcemap-codec: 1.4.8 sourcemap-codec: 1.4.8
dev: true
/mailcheck@1.1.1: /mailcheck@1.1.1:
resolution: {integrity: sha512-3WjL8+ZDouZwKlyJBMp/4LeziLFXgleOdsYu87piGcMLqhBzCsy2QFdbtAwv757TFC/rtqd738fgJw1tFQCSgA==} resolution: {integrity: sha512-3WjL8+ZDouZwKlyJBMp/4LeziLFXgleOdsYu87piGcMLqhBzCsy2QFdbtAwv757TFC/rtqd738fgJw1tFQCSgA==}
@ -13267,7 +13276,6 @@ packages:
/sourcemap-codec@1.4.8: /sourcemap-codec@1.4.8:
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
deprecated: Please use @jridgewell/sourcemap-codec instead deprecated: Please use @jridgewell/sourcemap-codec instead
dev: true
/sparkles@1.0.1: /sparkles@1.0.1:
resolution: {integrity: sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==} resolution: {integrity: sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==}
@ -13686,6 +13694,10 @@ packages:
resolution: {integrity: sha512-g9rPT3V1Q4WjWFZ/t5BdGC1mT/FpYnsLdBl+M5e6MlRkuE1RSR+R43wcY/3mKI59B9KEr+vxdWCuWNMD3oNHKA==} resolution: {integrity: sha512-g9rPT3V1Q4WjWFZ/t5BdGC1mT/FpYnsLdBl+M5e6MlRkuE1RSR+R43wcY/3mKI59B9KEr+vxdWCuWNMD3oNHKA==}
dev: true dev: true
/tabbable@6.1.1:
resolution: {integrity: sha512-4kl5w+nCB44EVRdO0g/UGoOp3vlwgycUVtkk/7DPyeLZUCuNFFKCFG6/t/DgHLrUPHjrZg6s5tNm+56Q2B0xyg==}
dev: false
/tapable@2.2.1: /tapable@2.2.1:
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -14776,7 +14788,6 @@ packages:
'@vue/runtime-dom': 3.2.45 '@vue/runtime-dom': 3.2.45
'@vue/server-renderer': 3.2.45(vue@3.2.45) '@vue/server-renderer': 3.2.45(vue@3.2.45)
'@vue/shared': 3.2.45 '@vue/shared': 3.2.45
dev: true
/vuedraggable@4.1.0(vue@3.2.45): /vuedraggable@4.1.0(vue@3.2.45):
resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==} resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}