fix(frontend): フォーカスの挙動を修正 (#14158)
* fix(frontend): 直前のパターンを記録するように * fix(frontend): フォーカス/タブ移動に関する挙動を調整 (#226) Cherry-pick commit e8c030673326871edf3623cf2b8675d68f9e1b13 Co-authored-by: taiyme <53635909+taiyme@users.noreply.github.com> * focusのデザイン修正 * move scripts * Modalにfocus trapを追加 * 記録するホットキーはレートリミット式にする * escキーのハンドリングをMkModalに統一 * fix * enterで子メニューを開けるように * lint * fix focus trap * improve switch accessibility * 一部のmodalのフォーカストラップが外れない問題を修正 * fix * fix * Revert "記録するホットキーはレートリミット式にする" This reverts commit 40a7509286a87911ad4cc06d9482e8a2e5d0e7e8. * Revert "fix(frontend): 直前のパターンを記録するように" This reverts commit 5372b2594023952cff34aa62253ed4efef15b5dd. * Revert "Revert "fix(frontend): 直前のパターンを記録するように"" This reverts commit a9bb52e799e110927ad92cd8f26af980819334e1. * Revert "Revert "記録するホットキーはレートリミット式にする"" This reverts commit bdac34273e0bc5f13604c7e2f9fa6b1321a0df3d. * 試験的にCypressでのFocustrapを無効化 * fix * fix focus-trap * Update Changelog * ✌️ * fix focustrap invocation logic * スクロールがsticky headerを考慮するように * 🎨 * スタイルの微調整 * 🎨 * remove deprecated key aliases * focusElementが足りなかったので修正 * preview系にfocus時スタイルが足りなかったので修正 * `returnFocusElement` -> `returnFocusTo` * lint * Update packages/frontend/src/components/MkModalWindow.vue * Apply suggestions from code review Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com> * keydownイベントをまとめる * use correct pesudo-element selector * fix * rename --------- Co-authored-by: taiyme <53635909+taiyme@users.noreply.github.com> Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
parent
121af778a0
commit
385969e9f5
@ -11,6 +11,8 @@
|
|||||||
### Client
|
### Client
|
||||||
- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善
|
- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善
|
||||||
- Enhance: 非ログイン時のハイライトTLのデザインを改善
|
- Enhance: 非ログイン時のハイライトTLのデザインを改善
|
||||||
|
- Enhance: フロントエンドのアクセシビリティ改善
|
||||||
|
(Based on https://github.com/taiyme/misskey/pull/226)
|
||||||
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
|
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
|
||||||
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
|
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
|
||||||
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
|
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
|
||||||
|
@ -153,7 +153,7 @@ onMounted(() => {
|
|||||||
background: linear-gradient(0deg, #ffee20, #eb7018);
|
background: linear-gradient(0deg, #ffee20, #eb7018);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -173,7 +173,7 @@ onMounted(() => {
|
|||||||
background: linear-gradient(0deg, #e1e1e1, #7c7c7c);
|
background: linear-gradient(0deg, #e1e1e1, #7c7c7c);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -250,7 +250,6 @@ function onMousedown(evt: MouseEvent): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: solid 2px var(--focus);
|
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,17 +87,7 @@ async function onClick() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
&:after {
|
outline-offset: 2px;
|
||||||
content: "";
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
top: -5px;
|
|
||||||
right: -5px;
|
|
||||||
bottom: -5px;
|
|
||||||
left: -5px;
|
|
||||||
border: 2px solid var(--focus);
|
|
||||||
border-radius: 32px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div style="position: relative;">
|
<div style="position: relative;">
|
||||||
<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1" @click="updateLastReadedAt">
|
<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" @click="updateLastReadedAt">
|
||||||
<div class="banner" :style="bannerStyle">
|
<div class="banner" :style="bannerStyle">
|
||||||
<div class="fade"></div>
|
<div class="fade"></div>
|
||||||
<div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div>
|
<div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div>
|
||||||
@ -80,6 +80,7 @@ const bannerStyle = computed(() => {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.eftoefju {
|
.eftoefju {
|
||||||
display: block;
|
display: block;
|
||||||
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@ -87,6 +88,22 @@ const bannerStyle = computed(() => {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: inset 0 0 0 2px var(--focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> .banner {
|
> .banner {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -40,6 +40,14 @@ const remaining = computed(() => {
|
|||||||
.link {
|
.link {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
.root {
|
||||||
|
box-shadow: inset 0 0 0 2px var(--focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
:leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
|
:leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
|
||||||
>
|
>
|
||||||
<div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
|
<div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
|
||||||
<MkMenu :items="items" :align="'left'" @close="$emit('closed')"/>
|
<MkMenu :items="items" :align="'left'" @close="emit('closed')"/>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</template>
|
</template>
|
||||||
|
@ -45,11 +45,11 @@ function toggle() {
|
|||||||
.label {
|
.label {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: '(';
|
content: '(';
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
content: ')';
|
content: ')';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')">
|
<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')" @esc="cancel()">
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<div v-if="icon" :class="$style.icon">
|
<div v-if="icon" :class="$style.icon">
|
||||||
<i :class="icon"></i>
|
<i :class="icon"></i>
|
||||||
@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onBeforeUnmount, onMounted, ref, shallowRef, computed } from 'vue';
|
import { ref, shallowRef, computed } from 'vue';
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
@ -156,10 +156,6 @@ function onBgClick() {
|
|||||||
if (props.cancelableByBgClick) cancel();
|
if (props.cancelableByBgClick) cancel();
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
function onKeydown(evt: KeyboardEvent) {
|
|
||||||
if (evt.key === 'Escape') cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onInputKeydown(evt: KeyboardEvent) {
|
function onInputKeydown(evt: KeyboardEvent) {
|
||||||
if (evt.key === 'Enter' && okButtonDisabledReason.value === null) {
|
if (evt.key === 'Enter' && okButtonDisabledReason.value === null) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
@ -167,14 +163,6 @@ function onInputKeydown(evt: KeyboardEvent) {
|
|||||||
ok();
|
ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
document.addEventListener('keydown', onKeydown);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
document.removeEventListener('keydown', onKeydown);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
@ -115,14 +115,14 @@ function onDragend() {
|
|||||||
background: rgba(#000, 0.05);
|
background: rgba(#000, 0.05);
|
||||||
|
|
||||||
> .label {
|
> .label {
|
||||||
&:before,
|
&::before,
|
||||||
&:after {
|
&::after {
|
||||||
background: #0b65a5;
|
background: #0b65a5;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.red {
|
&.red {
|
||||||
&:before,
|
&::before,
|
||||||
&:after {
|
&::after {
|
||||||
background: #c12113;
|
background: #c12113;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,14 +133,14 @@ function onDragend() {
|
|||||||
background: rgba(#000, 0.1);
|
background: rgba(#000, 0.1);
|
||||||
|
|
||||||
> .label {
|
> .label {
|
||||||
&:before,
|
&::before,
|
||||||
&:after {
|
&::after {
|
||||||
background: #0b588c;
|
background: #0b588c;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.red {
|
&.red {
|
||||||
&:before,
|
&::before,
|
||||||
&:after {
|
&::after {
|
||||||
background: #ce2212;
|
background: #ce2212;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,8 +159,8 @@ function onDragend() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .label {
|
> .label {
|
||||||
&:before,
|
&::before,
|
||||||
&:after {
|
&::after {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -181,8 +181,8 @@ function onDragend() {
|
|||||||
left: 0;
|
left: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
&:before,
|
&::before,
|
||||||
&:after {
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -190,14 +190,14 @@ function onDragend() {
|
|||||||
background: #0c7ac9;
|
background: #0c7ac9;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 57px;
|
left: 57px;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
top: 57px;
|
top: 57px;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
@ -205,8 +205,8 @@ function onDragend() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.red {
|
&.red {
|
||||||
&:before,
|
&::before,
|
||||||
&:after {
|
&::after {
|
||||||
background: #c12113;
|
background: #c12113;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -296,7 +296,7 @@ function onContextmenu(ev: MouseEvent) {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&.draghover {
|
&.draghover {
|
||||||
&:after {
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -5,7 +5,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
|
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
|
||||||
<input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" autocapitalize="off" @input="input()" @paste.stop="paste" @keydown.stop.prevent.enter="onEnter">
|
<input
|
||||||
|
ref="searchEl"
|
||||||
|
:value="q"
|
||||||
|
class="search"
|
||||||
|
data-prevent-emoji-insert
|
||||||
|
:class="{ filled: q != null && q != '' }"
|
||||||
|
:placeholder="i18n.ts.search"
|
||||||
|
type="search"
|
||||||
|
autocapitalize="off"
|
||||||
|
@input="input()"
|
||||||
|
@paste.stop="paste"
|
||||||
|
@keydown="onKeydown"
|
||||||
|
>
|
||||||
<!-- FirefoxのTabフォーカスが想定外の挙動となるためtabindex="-1"を追加 https://github.com/misskey-dev/misskey/issues/10744 -->
|
<!-- FirefoxのTabフォーカスが想定外の挙動となるためtabindex="-1"を追加 https://github.com/misskey-dev/misskey/issues/10744 -->
|
||||||
<div ref="emojisEl" class="emojis" tabindex="-1">
|
<div ref="emojisEl" class="emojis" tabindex="-1">
|
||||||
<section class="result">
|
<section class="result">
|
||||||
@ -139,6 +151,7 @@ const props = withDefaults(defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'chosen', v: string): void;
|
(ev: 'chosen', v: string): void;
|
||||||
|
(ev: 'esc'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const searchEl = shallowRef<HTMLInputElement>();
|
const searchEl = shallowRef<HTMLInputElement>();
|
||||||
@ -433,10 +446,19 @@ function paste(event: ClipboardEvent): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEnter(ev: KeyboardEvent) {
|
function onKeydown(ev: KeyboardEvent) {
|
||||||
if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
|
if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
|
||||||
|
if (ev.key === 'Enter') {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
|
if (ev.key === 'Escape') {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
emit('esc');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function done(query?: string): boolean | void {
|
function done(query?: string): boolean | void {
|
||||||
if (query == null) query = q.value;
|
if (query == null) query = q.value;
|
||||||
@ -702,11 +724,6 @@ defineExpose({
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: solid 2px var(--focus);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(0, 0, 0, 0.05);
|
background: rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
:manualShowing="manualShowing"
|
:manualShowing="manualShowing"
|
||||||
:src="src"
|
:src="src"
|
||||||
@click="modal?.close()"
|
@click="modal?.close()"
|
||||||
|
@esc="modal?.close()"
|
||||||
@opening="opening"
|
@opening="opening"
|
||||||
@close="emit('close')"
|
@close="emit('close')"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
@ -28,6 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
:asDrawer="type === 'drawer'"
|
:asDrawer="type === 'drawer'"
|
||||||
:max-height="maxHeight"
|
:max-height="maxHeight"
|
||||||
@chosen="chosen"
|
@chosen="chosen"
|
||||||
|
@esc="modal?.close()"
|
||||||
/>
|
/>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkA :to="`/play/${flash.id}`" class="vhpxefrk _panel" tabindex="-1">
|
<MkA :to="`/play/${flash.id}`" class="vhpxefrk _panel">
|
||||||
<article>
|
<article>
|
||||||
<header>
|
<header>
|
||||||
<h1 :title="flash.title">{{ flash.title }}</h1>
|
<h1 :title="flash.title">{{ flash.title }}</h1>
|
||||||
@ -39,6 +39,10 @@ const props = defineProps<{
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
> article {
|
> article {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
||||||
|
@ -7,10 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<div ref="rootEl" :class="$style.root" role="group" :aria-expanded="opened">
|
<div ref="rootEl" :class="$style.root" role="group" :aria-expanded="opened">
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle">
|
<button :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle">
|
||||||
<div :class="$style.headerIcon"><slot name="icon"></slot></div>
|
<div :class="$style.headerIcon"><slot name="icon"></slot></div>
|
||||||
<div :class="$style.headerText">
|
<div :class="$style.headerText">
|
||||||
<div>
|
<div :class="$style.headerTextMain">
|
||||||
<MkCondensedLine :minScale="2 / 3"><slot name="label"></slot></MkCondensedLine>
|
<MkCondensedLine :minScale="2 / 3"><slot name="label"></slot></MkCondensedLine>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.headerTextSub">
|
<div :class="$style.headerTextSub">
|
||||||
@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<i v-if="opened" class="ti ti-chevron-up icon"></i>
|
<i v-if="opened" class="ti ti-chevron-up icon"></i>
|
||||||
<i v-else class="ti ti-chevron-down icon"></i>
|
<i v-else class="ti ti-chevron-down icon"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : undefined, overflow: maxHeight ? `auto` : undefined }" :aria-hidden="!opened">
|
<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : undefined, overflow: maxHeight ? `auto` : undefined }" :aria-hidden="!opened">
|
||||||
@ -147,6 +147,10 @@ onMounted(() => {
|
|||||||
background: var(--buttonHoverBg);
|
background: var(--buttonHoverBg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
background: var(--buttonHoverBg);
|
background: var(--buttonHoverBg);
|
||||||
@ -190,6 +194,12 @@ onMounted(() => {
|
|||||||
padding-right: 12px;
|
padding-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.headerTextMain,
|
||||||
|
.headerTextSub {
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.headerTextSub {
|
.headerTextSub {
|
||||||
color: var(--fgTransparentWeak);
|
color: var(--fgTransparentWeak);
|
||||||
font-size: .85em;
|
font-size: .85em;
|
||||||
|
@ -185,17 +185,7 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
&:after {
|
outline-offset: 2px;
|
||||||
content: "";
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
top: -5px;
|
|
||||||
right: -5px;
|
|
||||||
bottom: -5px;
|
|
||||||
left: -5px;
|
|
||||||
border: 2px solid var(--focus);
|
|
||||||
border-radius: 32px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -83,7 +83,7 @@ function leaveHover(): void {
|
|||||||
|
|
||||||
> article {
|
> article {
|
||||||
> footer {
|
> footer {
|
||||||
&:before {
|
&::before {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,7 +139,7 @@ function leaveHover(): void {
|
|||||||
text-shadow: 0 0 8px #000;
|
text-shadow: 0 0 8px #000;
|
||||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
:enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined"
|
:enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined"
|
||||||
:leaveFromClass="defaultStore.state.animation && props.transition?.leaveFromClass || undefined"
|
:leaveFromClass="defaultStore.state.animation && props.transition?.leaveFromClass || undefined"
|
||||||
>
|
>
|
||||||
<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined"/>
|
<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"/>
|
||||||
<img v-show="!hide" key="img" ref="img" :height="imgHeight" :width="imgWidth" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async"/>
|
<img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal?.close()" @closed="emit('closed')">
|
<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal?.close()" @closed="emit('closed')" @esc="modal?.close()">
|
||||||
<div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }">
|
<div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }">
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<template v-for="item in items" :key="item.text">
|
<template v-for="item in items" :key="item.text">
|
||||||
|
@ -39,23 +39,37 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<audio
|
<audio
|
||||||
ref="audioEl"
|
ref="audioEl"
|
||||||
preload="metadata"
|
preload="metadata"
|
||||||
|
@keydown.prevent="() => {}"
|
||||||
>
|
>
|
||||||
<source :src="audio.url">
|
<source :src="audio.url">
|
||||||
</audio>
|
</audio>
|
||||||
<div :class="[$style.controlsChild, $style.controlsLeft]">
|
<div :class="[$style.controlsChild, $style.controlsLeft]">
|
||||||
<button class="_button" :class="$style.controlButton" @click="togglePlayPause">
|
<button
|
||||||
|
:class="['_button', $style.controlButton]"
|
||||||
|
tabindex="-1"
|
||||||
|
@click.stop="togglePlayPause"
|
||||||
|
>
|
||||||
<i v-if="isPlaying" class="ti ti-player-pause-filled"></i>
|
<i v-if="isPlaying" class="ti ti-player-pause-filled"></i>
|
||||||
<i v-else class="ti ti-player-play-filled"></i>
|
<i v-else class="ti ti-player-play-filled"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div :class="[$style.controlsChild, $style.controlsRight]">
|
<div :class="[$style.controlsChild, $style.controlsRight]">
|
||||||
<button class="_button" :class="$style.controlButton" @click="showMenu">
|
<button
|
||||||
|
:class="['_button', $style.controlButton]"
|
||||||
|
tabindex="-1"
|
||||||
|
@click.stop="() => {}"
|
||||||
|
@mousedown.prevent.stop="showMenu"
|
||||||
|
>
|
||||||
<i class="ti ti-settings"></i>
|
<i class="ti ti-settings"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div :class="[$style.controlsChild, $style.controlsTime]">{{ hms(elapsedTimeMs) }}</div>
|
<div :class="[$style.controlsChild, $style.controlsTime]">{{ hms(elapsedTimeMs) }}</div>
|
||||||
<div :class="[$style.controlsChild, $style.controlsVolume]">
|
<div :class="[$style.controlsChild, $style.controlsVolume]">
|
||||||
<button class="_button" :class="$style.controlButton" @click="toggleMute">
|
<button
|
||||||
|
:class="['_button', $style.controlButton]"
|
||||||
|
tabindex="-1"
|
||||||
|
@click.stop="toggleMute"
|
||||||
|
>
|
||||||
<i v-if="volume === 0" class="ti ti-volume-3"></i>
|
<i v-if="volume === 0" class="ti ti-volume-3"></i>
|
||||||
<i v-else class="ti ti-volume"></i>
|
<i v-else class="ti ti-volume"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -371,7 +385,7 @@ onDeactivated(() => {
|
|||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
|
||||||
&:focus {
|
&:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -437,6 +451,10 @@ onDeactivated(() => {
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
background-color: var(--accentedBg);
|
background-color: var(--accentedBg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,6 +39,7 @@ import XVideo from '@/components/MkMediaVideo.vue';
|
|||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
import { focusParent } from '@/scripts/focus.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
mediaList: Misskey.entities.DriveFile[];
|
mediaList: Misskey.entities.DriveFile[];
|
||||||
@ -49,7 +50,9 @@ const gallery = shallowRef<HTMLDivElement>();
|
|||||||
const pswpZIndex = os.claimZIndex('middle');
|
const pswpZIndex = os.claimZIndex('middle');
|
||||||
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
|
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
|
||||||
const count = computed(() => props.mediaList.filter(media => previewable(media)).length);
|
const count = computed(() => props.mediaList.filter(media => previewable(media)).length);
|
||||||
let lightbox: PhotoSwipeLightbox | null;
|
let lightbox: PhotoSwipeLightbox | null = null;
|
||||||
|
|
||||||
|
let activeEl: HTMLElement | null = null;
|
||||||
|
|
||||||
const popstateHandler = (): void => {
|
const popstateHandler = (): void => {
|
||||||
if (lightbox?.pswp && lightbox.pswp.isOpen === true) {
|
if (lightbox?.pswp && lightbox.pswp.isOpen === true) {
|
||||||
@ -60,7 +63,7 @@ const popstateHandler = (): void => {
|
|||||||
async function calcAspectRatio() {
|
async function calcAspectRatio() {
|
||||||
if (!gallery.value) return;
|
if (!gallery.value) return;
|
||||||
|
|
||||||
let img = props.mediaList[0];
|
const img = props.mediaList[0];
|
||||||
|
|
||||||
if (props.mediaList.length !== 1 || !(img.properties.width && img.properties.height)) {
|
if (props.mediaList.length !== 1 || !(img.properties.width && img.properties.height)) {
|
||||||
gallery.value.style.aspectRatio = '';
|
gallery.value.style.aspectRatio = '';
|
||||||
@ -131,6 +134,7 @@ onMounted(() => {
|
|||||||
bgOpacity: 1,
|
bgOpacity: 1,
|
||||||
showAnimationDuration: 100,
|
showAnimationDuration: 100,
|
||||||
hideAnimationDuration: 100,
|
hideAnimationDuration: 100,
|
||||||
|
returnFocus: false,
|
||||||
pswpModule: PhotoSwipe,
|
pswpModule: PhotoSwipe,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -159,39 +163,47 @@ onMounted(() => {
|
|||||||
lightbox.on('uiRegister', () => {
|
lightbox.on('uiRegister', () => {
|
||||||
lightbox?.pswp?.ui?.registerElement({
|
lightbox?.pswp?.ui?.registerElement({
|
||||||
name: 'altText',
|
name: 'altText',
|
||||||
className: 'pwsp__alt-text-container',
|
className: 'pswp__alt-text-container',
|
||||||
appendTo: 'wrapper',
|
appendTo: 'wrapper',
|
||||||
onInit: (el, pwsp) => {
|
onInit: (el, pswp) => {
|
||||||
let textBox = document.createElement('p');
|
const textBox = document.createElement('p');
|
||||||
textBox.className = 'pwsp__alt-text _acrylic';
|
textBox.className = 'pswp__alt-text _acrylic';
|
||||||
el.appendChild(textBox);
|
el.appendChild(textBox);
|
||||||
|
|
||||||
pwsp.on('change', () => {
|
pswp.on('change', () => {
|
||||||
textBox.textContent = pwsp.currSlide?.data.comment;
|
textBox.textContent = pswp.currSlide?.data.comment;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
lightbox.init();
|
lightbox.on('afterInit', () => {
|
||||||
|
activeEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||||
window.addEventListener('popstate', popstateHandler);
|
focusParent(activeEl, true, true);
|
||||||
|
lightbox?.pswp?.element?.focus({
|
||||||
lightbox.on('beforeOpen', () => {
|
preventScroll: true,
|
||||||
|
});
|
||||||
history.pushState(null, '', '#pswp');
|
history.pushState(null, '', '#pswp');
|
||||||
});
|
});
|
||||||
|
|
||||||
lightbox.on('close', () => {
|
lightbox.on('destroy', () => {
|
||||||
|
focusParent(activeEl, true, false);
|
||||||
|
activeEl = null;
|
||||||
if (window.location.hash === '#pswp') {
|
if (window.location.hash === '#pswp') {
|
||||||
history.back();
|
history.back();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener('popstate', popstateHandler);
|
||||||
|
|
||||||
|
lightbox.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('popstate', popstateHandler);
|
window.removeEventListener('popstate', popstateHandler);
|
||||||
lightbox?.destroy();
|
lightbox?.destroy();
|
||||||
lightbox = null;
|
lightbox = null;
|
||||||
|
activeEl = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const previewable = (file: Misskey.entities.DriveFile): boolean => {
|
const previewable = (file: Misskey.entities.DriveFile): boolean => {
|
||||||
@ -199,6 +211,16 @@ const previewable = (file: Misskey.entities.DriveFile): boolean => {
|
|||||||
// FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切
|
// FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切
|
||||||
return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type);
|
return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openGallery = () => {
|
||||||
|
if (props.mediaList.filter(media => previewable(media)).length > 0) {
|
||||||
|
lightbox?.loadAndOpen(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
openGallery,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
@ -298,7 +320,7 @@ const previewable = (file: Misskey.entities.DriveFile): boolean => {
|
|||||||
backdrop-filter: var(--modalBgFilter);
|
backdrop-filter: var(--modalBgFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pwsp__alt-text-container {
|
.pswp__alt-text-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -312,7 +334,7 @@ const previewable = (file: Misskey.entities.DriveFile): boolean => {
|
|||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pwsp__alt-text {
|
.pswp__alt-text {
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -481,7 +481,7 @@ onDeactivated(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
|
||||||
&:focus {
|
&:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -588,6 +588,10 @@ onDeactivated(() => {
|
|||||||
border-radius: 99rem;
|
border-radius: 99rem;
|
||||||
|
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.videoLoading {
|
.videoLoading {
|
||||||
@ -651,6 +655,10 @@ onDeactivated(() => {
|
|||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--accent);
|
background-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
|
import { nextTick, onMounted, onUnmounted, provide, shallowRef, watch } from 'vue';
|
||||||
import MkMenu from './MkMenu.vue';
|
import MkMenu from './MkMenu.vue';
|
||||||
import { MenuItem } from '@/types/menu.js';
|
import { MenuItem } from '@/types/menu.js';
|
||||||
|
|
||||||
@ -19,7 +19,6 @@ const props = defineProps<{
|
|||||||
targetElement: HTMLElement;
|
targetElement: HTMLElement;
|
||||||
rootElement: HTMLElement;
|
rootElement: HTMLElement;
|
||||||
width?: number;
|
width?: number;
|
||||||
viaKeyboard?: boolean;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -27,6 +26,8 @@ const emit = defineEmits<{
|
|||||||
(ev: 'actioned'): void;
|
(ev: 'actioned'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
provide('isNestingMenu', true);
|
||||||
|
|
||||||
const el = shallowRef<HTMLElement>();
|
const el = shallowRef<HTMLElement>();
|
||||||
const align = 'left';
|
const align = 'left';
|
||||||
|
|
||||||
|
@ -4,23 +4,42 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div role="menu">
|
<div role="menu" @focusin.passive.stop="() => {}">
|
||||||
<div
|
<div
|
||||||
ref="itemsEl" v-hotkey="keymap"
|
ref="itemsEl"
|
||||||
|
v-hotkey="keymap"
|
||||||
|
tabindex="0"
|
||||||
class="_popup _shadow"
|
class="_popup _shadow"
|
||||||
:class="[$style.root, { [$style.center]: align === 'center', [$style.asDrawer]: asDrawer }]"
|
:class="{
|
||||||
:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
|
[$style.root]: true,
|
||||||
@contextmenu.self="e => e.preventDefault()"
|
[$style.center]: align === 'center',
|
||||||
|
[$style.asDrawer]: asDrawer,
|
||||||
|
}"
|
||||||
|
:style="{
|
||||||
|
width: (width && !asDrawer) ? `${width}px` : '',
|
||||||
|
maxHeight: maxHeight ? `${maxHeight}px` : '',
|
||||||
|
}"
|
||||||
|
@keydown.stop="() => {}"
|
||||||
|
@contextmenu.self.prevent="() => {}"
|
||||||
>
|
>
|
||||||
<template v-for="(item, i) in (items2 ?? [])">
|
<template v-for="item in (items2 ?? [])">
|
||||||
<div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div>
|
<div v-if="item.type === 'divider'" role="separator" tabindex="-1" :class="$style.divider"></div>
|
||||||
<span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]">
|
<span v-else-if="item.type === 'label'" role="menuitem" tabindex="-1" :class="[$style.label, $style.item]">
|
||||||
<span style="opacity: 0.7;">{{ item.text }}</span>
|
<span style="opacity: 0.7;">{{ item.text }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="item.type === 'pending'" role="menuitem" :tabindex="i" :class="[$style.pending, $style.item]">
|
<span v-else-if="item.type === 'pending'" role="menuitem" tabindex="0" :class="[$style.pending, $style.item]">
|
||||||
<span><MkEllipsis/></span>
|
<span><MkEllipsis/></span>
|
||||||
</span>
|
</span>
|
||||||
<MkA v-else-if="item.type === 'link'" role="menuitem" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
<MkA
|
||||||
|
v-else-if="item.type === 'link'"
|
||||||
|
role="menuitem"
|
||||||
|
tabindex="0"
|
||||||
|
:class="['_button', $style.item]"
|
||||||
|
:to="item.to"
|
||||||
|
@click.passive="close(true)"
|
||||||
|
@mouseenter.passive="onItemMouseEnter"
|
||||||
|
@mouseleave.passive="onItemMouseLeave"
|
||||||
|
>
|
||||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||||
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
|
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
|
||||||
<div :class="$style.item_content">
|
<div :class="$style.item_content">
|
||||||
@ -28,20 +47,48 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</MkA>
|
</MkA>
|
||||||
<a v-else-if="item.type === 'a'" role="menuitem" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
<a
|
||||||
|
v-else-if="item.type === 'a'"
|
||||||
|
role="menuitem"
|
||||||
|
tabindex="0"
|
||||||
|
:class="['_button', $style.item]"
|
||||||
|
:href="item.href"
|
||||||
|
:target="item.target"
|
||||||
|
:download="item.download"
|
||||||
|
@click.passive="close(true)"
|
||||||
|
@mouseenter.passive="onItemMouseEnter"
|
||||||
|
@mouseleave.passive="onItemMouseLeave"
|
||||||
|
>
|
||||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||||
<div :class="$style.item_content">
|
<div :class="$style.item_content">
|
||||||
<span :class="$style.item_content_text">{{ item.text }}</span>
|
<span :class="$style.item_content_text">{{ item.text }}</span>
|
||||||
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<button v-else-if="item.type === 'user'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
<button
|
||||||
|
v-else-if="item.type === 'user'"
|
||||||
|
role="menuitem"
|
||||||
|
tabindex="0"
|
||||||
|
:class="['_button', $style.item, { [$style.active]: item.active }]"
|
||||||
|
@click.prevent="item.active ? close(false) : clicked(item.action, $event)"
|
||||||
|
@mouseenter.passive="onItemMouseEnter"
|
||||||
|
@mouseleave.passive="onItemMouseLeave"
|
||||||
|
>
|
||||||
<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
|
<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
|
||||||
<div v-if="item.indicate" :class="$style.item_content">
|
<div v-if="item.indicate" :class="$style.item_content">
|
||||||
<span :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
<span :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
<button
|
||||||
|
v-else-if="item.type === 'switch'"
|
||||||
|
role="menuitemcheckbox"
|
||||||
|
tabindex="0"
|
||||||
|
:class="['_button', $style.item]"
|
||||||
|
:disabled="unref(item.disabled)"
|
||||||
|
@click.prevent="switchItem(item)"
|
||||||
|
@mouseenter.passive="onItemMouseEnter"
|
||||||
|
@mouseleave.passive="onItemMouseLeave"
|
||||||
|
>
|
||||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||||
<MkSwitchButton v-else :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
|
<MkSwitchButton v-else :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
|
||||||
<div :class="$style.item_content">
|
<div :class="$style.item_content">
|
||||||
@ -49,29 +96,61 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkSwitchButton v-if="item.icon" :class="[$style.switchButton, $style.caret]" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
|
<MkSwitchButton v-if="item.icon" :class="[$style.switchButton, $style.caret]" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button v-else-if="item.type === 'radio'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showRadioOptions(item, $event)" @click="!preferClick ? null : showRadioOptions(item, $event)">
|
<button
|
||||||
|
v-else-if="item.type === 'radio'"
|
||||||
|
role="menuitem"
|
||||||
|
tabindex="0"
|
||||||
|
:class="['_button', $style.item, $style.parent, { [$style.active]: childShowingItem === item }]"
|
||||||
|
:disabled="unref(item.disabled)"
|
||||||
|
@mouseenter.prevent="preferClick ? null : showRadioOptions(item, $event)"
|
||||||
|
@keydown.enter.prevent="preferClick ? null : showRadioOptions(item, $event)"
|
||||||
|
@click.prevent="!preferClick ? null : showRadioOptions(item, $event)"
|
||||||
|
>
|
||||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
|
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
|
||||||
<div :class="$style.item_content">
|
<div :class="$style.item_content">
|
||||||
<span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
|
<span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
|
||||||
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
|
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button v-else-if="item.type === 'radioOption'" :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.radioActive]: item.active }]" @click="clicked(item.action, $event, false)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
<button
|
||||||
|
v-else-if="item.type === 'radioOption'"
|
||||||
|
role="menuitemradio"
|
||||||
|
tabindex="0"
|
||||||
|
:class="['_button', $style.item, $style.radio, { [$style.active]: unref(item.active) }]"
|
||||||
|
@click.prevent="unref(item.active) ? null : clicked(item.action, $event, false)"
|
||||||
|
@mouseenter.passive="onItemMouseEnter"
|
||||||
|
@mouseleave.passive="onItemMouseLeave"
|
||||||
|
>
|
||||||
<div :class="$style.icon">
|
<div :class="$style.icon">
|
||||||
<span :class="[$style.radio, { [$style.radioChecked]: item.active }]"></span>
|
<span :class="[$style.radioIcon, { [$style.radioChecked]: unref(item.active) }]"></span>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.item_content">
|
<div :class="$style.item_content">
|
||||||
<span :class="$style.item_content_text">{{ item.text }}</span>
|
<span :class="$style.item_content_text">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button v-else-if="item.type === 'parent'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)">
|
<button
|
||||||
|
v-else-if="item.type === 'parent'"
|
||||||
|
role="menuitem"
|
||||||
|
tabindex="0"
|
||||||
|
:class="['_button', $style.item, $style.parent, { [$style.active]: childShowingItem === item }]"
|
||||||
|
@mouseenter.prevent="preferClick ? null : showChildren(item, $event)"
|
||||||
|
@keydown.enter.prevent="preferClick ? null : showChildren(item, $event)"
|
||||||
|
@click.prevent="!preferClick ? null : showChildren(item, $event)"
|
||||||
|
>
|
||||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
|
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
|
||||||
<div :class="$style.item_content">
|
<div :class="$style.item_content">
|
||||||
<span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
|
<span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
|
||||||
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
|
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: getValue(item.active) }]" :disabled="getValue(item.active)" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
<button
|
||||||
|
v-else role="menuitem"
|
||||||
|
tabindex="0"
|
||||||
|
:class="['_button', $style.item, { [$style.danger]: item.danger, [$style.active]: unref(item.active) }]"
|
||||||
|
@click.prevent="unref(item.active) ? close(false) : clicked(item.action, $event)"
|
||||||
|
@mouseenter.passive="onItemMouseEnter"
|
||||||
|
@mouseleave.passive="onItemMouseLeave"
|
||||||
|
>
|
||||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||||
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
|
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
|
||||||
<div :class="$style.item_content">
|
<div :class="$style.item_content">
|
||||||
@ -80,25 +159,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<span v-if="items2 == null || items2.length === 0" :class="[$style.none, $style.item]">
|
<span v-if="items2 == null || items2.length === 0" tabindex="-1" :class="[$style.none, $style.item]">
|
||||||
<span>{{ i18n.ts.none }}</span>
|
<span>{{ i18n.ts.none }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="childMenu">
|
<div v-if="childMenu">
|
||||||
<XChild ref="child" :items="childMenu" :targetElement="childTarget!" :rootElement="itemsEl!" showing @actioned="childActioned" @close="close(false)"/>
|
<XChild ref="child" :items="childMenu" :targetElement="childTarget!" :rootElement="itemsEl!" @actioned="childActioned" @closed="closeChild"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ComputedRef, computed, defineAsyncComponent, isRef, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
|
import { computed, defineAsyncComponent, inject, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, unref, watch } from 'vue';
|
||||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
|
||||||
import MkSwitchButton from '@/components/MkSwitch.button.vue';
|
import MkSwitchButton from '@/components/MkSwitch.button.vue';
|
||||||
import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js';
|
import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { isTouchUsing } from '@/scripts/touch.js';
|
import { isTouchUsing } from '@/scripts/touch.js';
|
||||||
import { type Keymap } from '@/scripts/hotkey.js';
|
import { type Keymap } from '@/scripts/hotkey.js';
|
||||||
|
import { isFocusable } from '@/scripts/focus.js';
|
||||||
|
import { getNodeOrNull } from '@/scripts/get-dom-node-or-null.js';
|
||||||
|
|
||||||
const childrenCache = new WeakMap<MenuParent, MenuItem[]>();
|
const childrenCache = new WeakMap<MenuParent, MenuItem[]>();
|
||||||
</script>
|
</script>
|
||||||
@ -108,7 +188,6 @@ const XChild = defineAsyncComponent(() => import('./MkMenu.child.vue'));
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
items: MenuItem[];
|
items: MenuItem[];
|
||||||
viaKeyboard?: boolean;
|
|
||||||
asDrawer?: boolean;
|
asDrawer?: boolean;
|
||||||
align?: 'center' | string;
|
align?: 'center' | string;
|
||||||
width?: number;
|
width?: number;
|
||||||
@ -120,7 +199,9 @@ const emit = defineEmits<{
|
|||||||
(ev: 'hide'): void;
|
(ev: 'hide'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const itemsEl = shallowRef<HTMLDivElement>();
|
const isNestingMenu = inject<boolean>('isNestingMenu', false);
|
||||||
|
|
||||||
|
const itemsEl = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
const items2 = ref<InnerMenuItem[]>();
|
const items2 = ref<InnerMenuItem[]>();
|
||||||
|
|
||||||
@ -177,25 +258,19 @@ function childActioned() {
|
|||||||
close(true);
|
close(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onGlobalMousedown = (event: MouseEvent) => {
|
|
||||||
if (childTarget.value && (event.target === childTarget.value || childTarget.value.contains(event.target as Node))) return;
|
|
||||||
if (child.value && child.value.checkHit(event)) return;
|
|
||||||
closeChild();
|
|
||||||
};
|
|
||||||
|
|
||||||
let childCloseTimer: null | number = null;
|
let childCloseTimer: null | number = null;
|
||||||
|
|
||||||
function onItemMouseEnter(item) {
|
function onItemMouseEnter() {
|
||||||
childCloseTimer = window.setTimeout(() => {
|
childCloseTimer = window.setTimeout(() => {
|
||||||
closeChild();
|
closeChild();
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onItemMouseLeave(item) {
|
function onItemMouseLeave() {
|
||||||
if (childCloseTimer) window.clearTimeout(childCloseTimer);
|
if (childCloseTimer) window.clearTimeout(childCloseTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showRadioOptions(item: MenuRadio, ev: MouseEvent) {
|
async function showRadioOptions(item: MenuRadio, ev: Event) {
|
||||||
const children: MenuItem[] = Object.keys(item.options).map<MenuRadioOption>(key => {
|
const children: MenuItem[] = Object.keys(item.options).map<MenuRadioOption>(key => {
|
||||||
const value = item.options[key];
|
const value = item.options[key];
|
||||||
return {
|
return {
|
||||||
@ -210,7 +285,7 @@ async function showRadioOptions(item: MenuRadio, ev: MouseEvent) {
|
|||||||
|
|
||||||
if (props.asDrawer) {
|
if (props.asDrawer) {
|
||||||
os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => {
|
os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => {
|
||||||
emit('close');
|
close(false);
|
||||||
});
|
});
|
||||||
emit('hide');
|
emit('hide');
|
||||||
} else {
|
} else {
|
||||||
@ -220,7 +295,7 @@ async function showRadioOptions(item: MenuRadio, ev: MouseEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showChildren(item: MenuParent, ev: MouseEvent) {
|
async function showChildren(item: MenuParent, ev: Event) {
|
||||||
const children: MenuItem[] = await (async () => {
|
const children: MenuItem[] = await (async () => {
|
||||||
if (childrenCache.has(item)) {
|
if (childrenCache.has(item)) {
|
||||||
return childrenCache.get(item)!;
|
return childrenCache.get(item)!;
|
||||||
@ -237,7 +312,7 @@ async function showChildren(item: MenuParent, ev: MouseEvent) {
|
|||||||
|
|
||||||
if (props.asDrawer) {
|
if (props.asDrawer) {
|
||||||
os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => {
|
os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => {
|
||||||
emit('close');
|
close(false);
|
||||||
});
|
});
|
||||||
emit('hide');
|
emit('hide');
|
||||||
} else {
|
} else {
|
||||||
@ -256,15 +331,11 @@ function clicked(fn: MenuAction, ev: MouseEvent, doClose = true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function close(actioned = false) {
|
function close(actioned = false) {
|
||||||
|
disposeHandlers();
|
||||||
|
nextTick(() => {
|
||||||
|
closeChild();
|
||||||
emit('close', actioned);
|
emit('close', actioned);
|
||||||
}
|
});
|
||||||
|
|
||||||
function focusUp() {
|
|
||||||
focusPrev(document.activeElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
function focusDown() {
|
|
||||||
focusNext(document.activeElement);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchItem(item: MenuSwitch & { ref: any }) {
|
function switchItem(item: MenuSwitch & { ref: any }) {
|
||||||
@ -272,25 +343,75 @@ function switchItem(item: MenuSwitch & { ref: any }) {
|
|||||||
item.ref = !item.ref;
|
item.ref = !item.ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getValue<T>(item?: ComputedRef<T> | T) {
|
function focusUp() {
|
||||||
return isRef(item) ? item.value : item;
|
if (disposed) return;
|
||||||
|
if (!itemsEl.value?.contains(document.activeElement)) return;
|
||||||
|
|
||||||
|
const focusableElements = Array.from(itemsEl.value.children).filter(isFocusable);
|
||||||
|
const activeIndex = focusableElements.findIndex(el => el === document.activeElement);
|
||||||
|
const targetIndex = (activeIndex !== -1 && activeIndex !== 0) ? (activeIndex - 1) : (focusableElements.length - 1);
|
||||||
|
const targetElement = focusableElements.at(targetIndex) ?? itemsEl.value;
|
||||||
|
|
||||||
|
targetElement.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function focusDown() {
|
||||||
|
if (disposed) return;
|
||||||
|
if (!itemsEl.value?.contains(document.activeElement)) return;
|
||||||
|
|
||||||
|
const focusableElements = Array.from(itemsEl.value.children).filter(isFocusable);
|
||||||
|
const activeIndex = focusableElements.findIndex(el => el === document.activeElement);
|
||||||
|
const targetIndex = (activeIndex !== -1 && activeIndex !== (focusableElements.length - 1)) ? (activeIndex + 1) : 0;
|
||||||
|
const targetElement = focusableElements.at(targetIndex) ?? itemsEl.value;
|
||||||
|
|
||||||
|
targetElement.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
const onGlobalFocusin = (ev: FocusEvent) => {
|
||||||
|
if (disposed) return;
|
||||||
|
if (itemsEl.value?.parentElement?.contains(getNodeOrNull(ev.target))) return;
|
||||||
|
nextTick(() => {
|
||||||
|
if (itemsEl.value != null && isFocusable(itemsEl.value)) {
|
||||||
|
itemsEl.value.focus({ preventScroll: true });
|
||||||
|
nextTick(() => focusDown());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onGlobalMousedown = (ev: MouseEvent) => {
|
||||||
|
if (disposed) return;
|
||||||
|
if (childTarget.value?.contains(getNodeOrNull(ev.target))) return;
|
||||||
|
if (child.value?.checkHit(ev)) return;
|
||||||
|
closeChild();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupHandlers = () => {
|
||||||
|
if (!isNestingMenu) {
|
||||||
|
document.addEventListener('focusin', onGlobalFocusin, { passive: true });
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', onGlobalMousedown, { passive: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
let disposed = false;
|
||||||
|
|
||||||
|
const disposeHandlers = () => {
|
||||||
|
disposed = true;
|
||||||
|
if (!isNestingMenu) {
|
||||||
|
document.removeEventListener('focusin', onGlobalFocusin);
|
||||||
|
}
|
||||||
|
document.removeEventListener('mousedown', onGlobalMousedown);
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.viaKeyboard) {
|
setupHandlers();
|
||||||
nextTick(() => {
|
|
||||||
if (itemsEl.value) focusNext(itemsEl.value.children[0], true, false);
|
if (!isNestingMenu) {
|
||||||
});
|
nextTick(() => itemsEl.value?.focus({ preventScroll: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: アクティブな要素までスクロール
|
|
||||||
//itemsEl.scrollTo();
|
|
||||||
|
|
||||||
document.addEventListener('mousedown', onGlobalMousedown, { passive: true });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
document.removeEventListener('mousedown', onGlobalMousedown);
|
disposeHandlers();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -303,6 +424,10 @@ onBeforeUnmount(() => {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
&.center {
|
&.center {
|
||||||
> .item {
|
> .item {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -320,7 +445,7 @@ onBeforeUnmount(() => {
|
|||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
padding: 12px 24px;
|
padding: 12px 24px;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
width: calc(100% - 24px);
|
width: calc(100% - 24px);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
@ -350,8 +475,10 @@ onBeforeUnmount(() => {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
text-decoration: none !important;
|
||||||
|
color: var(--menuFg, var(--fg));
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -365,56 +492,56 @@ onBeforeUnmount(() => {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:disabled):hover {
|
&:focus-visible {
|
||||||
color: var(--accent);
|
outline: none;
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
&:before {
|
&:not(:hover):not(:active)::before {
|
||||||
background: var(--accentedBg);
|
outline: var(--focus) solid 2px;
|
||||||
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:not(:disabled) {
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible:active,
|
||||||
|
&:focus-visible.active {
|
||||||
|
color: var(--menuHoverFg, var(--accent));
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: var(--menuHoverBg, var(--accentedBg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:focus-visible):active,
|
||||||
|
&:not(:focus-visible).active {
|
||||||
|
color: var(--menuActiveFg, var(--fgOnAccent));
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: var(--menuActiveBg, var(--accent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
&.danger {
|
&.danger {
|
||||||
color: #ff2a2a;
|
--menuFg: #ff2a2a;
|
||||||
|
--menuHoverFg: #fff;
|
||||||
&:hover {
|
--menuHoverBg: #ff4242;
|
||||||
color: #fff;
|
--menuActiveFg: #fff;
|
||||||
|
--menuActiveBg: #d42e2e;
|
||||||
&:before {
|
|
||||||
background: #ff4242;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&.radio {
|
||||||
color: #fff;
|
--menuActiveFg: var(--accent);
|
||||||
|
--menuActiveBg: var(--accentedBg);
|
||||||
&:before {
|
|
||||||
background: #d42e2e !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active,
|
&.parent {
|
||||||
&.active {
|
--menuActiveFg: var(--accent);
|
||||||
color: var(--fgOnAccent) !important;
|
--menuActiveBg: var(--accentedBg);
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
background: var(--accent) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.radioActive {
|
|
||||||
color: var(--accent) !important;
|
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
background-color: var(--accentedBg) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(:active):focus-visible {
|
|
||||||
box-shadow: 0 0 0 2px var(--focus) inset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.label {
|
&.label {
|
||||||
@ -432,22 +559,6 @@ onBeforeUnmount(() => {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.parent {
|
|
||||||
pointer-events: auto;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
cursor: default;
|
|
||||||
|
|
||||||
&.childShowing {
|
|
||||||
color: var(--accent);
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
background: var(--accentedBg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item_content {
|
.item_content {
|
||||||
@ -466,18 +577,6 @@ onBeforeUnmount(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
user-select: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switchDisabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switchButton {
|
.switchButton {
|
||||||
margin-left: -2px;
|
margin-left: -2px;
|
||||||
--height: 1.35em;
|
--height: 1.35em;
|
||||||
@ -489,14 +588,6 @@ onBeforeUnmount(() => {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.switchInput {
|
|
||||||
position: absolute;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@ -525,12 +616,12 @@ onBeforeUnmount(() => {
|
|||||||
border-top: solid 0.5px var(--divider);
|
border-top: solid 0.5px var(--divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio {
|
.radioIcon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
vertical-align: -.125em;
|
vertical-align: -0.125em;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: solid 2px var(--divider);
|
border: solid 2px var(--divider);
|
||||||
background-color: var(--panel);
|
background-color: var(--panel);
|
||||||
|
@ -30,9 +30,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
[$style.transition_modal_leaveTo]: transitionName === 'modal',
|
[$style.transition_modal_leaveTo]: transitionName === 'modal',
|
||||||
[$style.transition_send_leaveTo]: transitionName === 'send',
|
[$style.transition_send_leaveTo]: transitionName === 'send',
|
||||||
})"
|
})"
|
||||||
:duration="transitionDuration" appear @afterLeave="emit('closed')" @enter="emit('opening')" @afterEnter="onOpened"
|
:duration="transitionDuration" appear @afterLeave="onClosed" @enter="emit('opening')" @afterEnter="onOpened"
|
||||||
>
|
>
|
||||||
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
|
<div v-show="manualShowing != null ? manualShowing : showing" ref="modalRootEl" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
|
||||||
<div data-cy-bg :data-cy-transparent="isEnableBgTransparent" class="_modalBg" :class="[$style.bg, { [$style.bgTransparent]: isEnableBgTransparent }]" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
|
<div data-cy-bg :data-cy-transparent="isEnableBgTransparent" class="_modalBg" :class="[$style.bg, { [$style.bgTransparent]: isEnableBgTransparent }]" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
|
||||||
<div ref="content" :class="[$style.content, { [$style.fixed]: fixed }]" :style="{ zIndex }" @click.self="onBgClick">
|
<div ref="content" :class="[$style.content, { [$style.fixed]: fixed }]" :style="{ zIndex }" @click.self="onBgClick">
|
||||||
<slot :max-height="maxHeight" :type="type"></slot>
|
<slot :max-height="maxHeight" :type="type"></slot>
|
||||||
@ -48,6 +48,8 @@ import { isTouchUsing } from '@/scripts/touch.js';
|
|||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { deviceKind } from '@/scripts/device-kind.js';
|
import { deviceKind } from '@/scripts/device-kind.js';
|
||||||
import { type Keymap } from '@/scripts/hotkey.js';
|
import { type Keymap } from '@/scripts/hotkey.js';
|
||||||
|
import { focusTrap } from '@/scripts/focus-trap.js';
|
||||||
|
import { focusParent } from '@/scripts/focus.js';
|
||||||
|
|
||||||
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;
|
||||||
@ -69,6 +71,7 @@ const props = withDefaults(defineProps<{
|
|||||||
zPriority?: 'low' | 'middle' | 'high';
|
zPriority?: 'low' | 'middle' | 'high';
|
||||||
noOverlap?: boolean;
|
noOverlap?: boolean;
|
||||||
transparentBg?: boolean;
|
transparentBg?: boolean;
|
||||||
|
returnFocusTo?: HTMLElement | null;
|
||||||
}>(), {
|
}>(), {
|
||||||
manualShowing: null,
|
manualShowing: null,
|
||||||
src: null,
|
src: null,
|
||||||
@ -77,6 +80,7 @@ const props = withDefaults(defineProps<{
|
|||||||
zPriority: 'low',
|
zPriority: 'low',
|
||||||
noOverlap: true,
|
noOverlap: true,
|
||||||
transparentBg: false,
|
transparentBg: false,
|
||||||
|
returnFocusTo: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -94,6 +98,7 @@ const maxHeight = ref<number>();
|
|||||||
const fixed = ref(false);
|
const fixed = ref(false);
|
||||||
const transformOrigin = ref('center');
|
const transformOrigin = ref('center');
|
||||||
const showing = ref(true);
|
const showing = ref(true);
|
||||||
|
const modalRootEl = shallowRef<HTMLElement>();
|
||||||
const content = shallowRef<HTMLElement>();
|
const content = shallowRef<HTMLElement>();
|
||||||
const zIndex = os.claimZIndex(props.zPriority);
|
const zIndex = os.claimZIndex(props.zPriority);
|
||||||
const useSendAnime = ref(false);
|
const useSendAnime = ref(false);
|
||||||
@ -132,6 +137,7 @@ const transitionDuration = computed((() =>
|
|||||||
: 0
|
: 0
|
||||||
));
|
));
|
||||||
|
|
||||||
|
let releaseFocusTrap: (() => void) | null = null;
|
||||||
let contentClicking = false;
|
let contentClicking = false;
|
||||||
|
|
||||||
function close(opts: { useSendAnimation?: boolean } = {}) {
|
function close(opts: { useSendAnimation?: boolean } = {}) {
|
||||||
@ -296,6 +302,10 @@ const onOpened = () => {
|
|||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onClosed = () => {
|
||||||
|
emit('closed');
|
||||||
|
};
|
||||||
|
|
||||||
const alignObserver = new ResizeObserver((entries, observer) => {
|
const alignObserver = new ResizeObserver((entries, observer) => {
|
||||||
align();
|
align();
|
||||||
});
|
});
|
||||||
@ -313,6 +323,20 @@ onMounted(() => {
|
|||||||
align();
|
align();
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
|
watch([showing, () => props.manualShowing], ([showing, manualShowing]) => {
|
||||||
|
if (manualShowing === true || (manualShowing == null && showing === true)) {
|
||||||
|
if (modalRootEl.value != null) {
|
||||||
|
const { release } = focusTrap(modalRootEl.value);
|
||||||
|
|
||||||
|
releaseFocusTrap = release;
|
||||||
|
modalRootEl.value.focus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
releaseFocusTrap?.();
|
||||||
|
focusParent(props.returnFocusTo ?? props.src, true, false);
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
alignObserver.observe(content.value!);
|
alignObserver.observe(content.value!);
|
||||||
});
|
});
|
||||||
|
@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" :preferType="'dialog'" @click="onBgClick" @closed="$emit('closed')">
|
<MkModal ref="modal" :preferType="'dialog'" @click="onBgClick" @closed="emit('closed')" @esc="emit('esc')">
|
||||||
<div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }" @keydown="onKeydown">
|
<div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }">
|
||||||
<div ref="headerEl" :class="$style.header">
|
<div ref="headerEl" :class="$style.header">
|
||||||
<button v-if="withOkButton" :class="$style.headerButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button>
|
<button v-if="withOkButton" :class="$style.headerButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button>
|
||||||
<span :class="$style.title">
|
<span :class="$style.title">
|
||||||
@ -42,6 +42,7 @@ const emit = defineEmits<{
|
|||||||
(event: 'close'): void;
|
(event: 'close'): void;
|
||||||
(event: 'closed'): void;
|
(event: 'closed'): void;
|
||||||
(event: 'ok'): void;
|
(event: 'ok'): void;
|
||||||
|
(event: 'esc'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
@ -58,14 +59,6 @@ const onBgClick = () => {
|
|||||||
emit('click');
|
emit('click');
|
||||||
};
|
};
|
||||||
|
|
||||||
const onKeydown = (evt) => {
|
|
||||||
if (evt.which === 27) { // Esc
|
|
||||||
evt.preventDefault();
|
|
||||||
evt.stopPropagation();
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const ro = new ResizeObserver((entries, observer) => {
|
const ro = new ResizeObserver((entries, observer) => {
|
||||||
if (rootEl.value == null || headerEl.value == null) return;
|
if (rootEl.value == null || headerEl.value == null) return;
|
||||||
bodyWidth.value = rootEl.value.offsetWidth;
|
bodyWidth.value = rootEl.value.offsetWidth;
|
||||||
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
ref="rootEl"
|
ref="rootEl"
|
||||||
v-hotkey="keymap"
|
v-hotkey="keymap"
|
||||||
:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
|
:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
|
||||||
:tabindex="!isDeleted ? '-1' : undefined"
|
:tabindex="isDeleted ? '-1' : '0'"
|
||||||
>
|
>
|
||||||
<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
|
<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
|
||||||
<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
|
<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
|
||||||
@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</template>
|
</template>
|
||||||
</I18n>
|
</I18n>
|
||||||
<div :class="$style.renoteInfo">
|
<div :class="$style.renoteInfo">
|
||||||
<button ref="renoteTime" :class="$style.renoteTime" class="_button" @click="showRenoteMenu()">
|
<button ref="renoteTime" :class="$style.renoteTime" class="_button" @mousedown.prevent="showRenoteMenu()">
|
||||||
<i class="ti ti-dots" :class="$style.renoteMenu"></i>
|
<i class="ti ti-dots" :class="$style.renoteMenu"></i>
|
||||||
<MkTime :time="note.createdAt"/>
|
<MkTime :time="note.createdAt"/>
|
||||||
</button>
|
</button>
|
||||||
@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="appearNote.files && appearNote.files.length > 0">
|
<div v-if="appearNote.files && appearNote.files.length > 0">
|
||||||
<MkMediaList :mediaList="appearNote.files"/>
|
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
|
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
|
||||||
<div v-if="isEnabledUrlPreview">
|
<div v-if="isEnabledUrlPreview">
|
||||||
@ -110,7 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
ref="renoteButton"
|
ref="renoteButton"
|
||||||
:class="$style.footerButton"
|
:class="$style.footerButton"
|
||||||
class="_button"
|
class="_button"
|
||||||
@mousedown="renote()"
|
@mousedown.prevent="renote()"
|
||||||
>
|
>
|
||||||
<i class="ti ti-repeat"></i>
|
<i class="ti ti-repeat"></i>
|
||||||
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
||||||
@ -125,10 +125,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<i v-else class="ti ti-plus"></i>
|
<i v-else class="ti ti-plus"></i>
|
||||||
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
|
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()">
|
||||||
<i class="ti ti-paperclip"></i>
|
<i class="ti ti-paperclip"></i>
|
||||||
</button>
|
</button>
|
||||||
<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="showMenu()">
|
<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown.prevent="showMenu()">
|
||||||
<i class="ti ti-dots"></i>
|
<i class="ti ti-dots"></i>
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
@ -175,7 +175,6 @@ import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
|||||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
|
||||||
import { checkWordMute } from '@/scripts/check-word-mute.js';
|
import { checkWordMute } from '@/scripts/check-word-mute.js';
|
||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
import number from '@/filters/number.js';
|
import number from '@/filters/number.js';
|
||||||
@ -199,6 +198,7 @@ import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
|||||||
import { shouldCollapsed } from '@/scripts/collapsed.js';
|
import { shouldCollapsed } from '@/scripts/collapsed.js';
|
||||||
import { isEnabledUrlPreview } from '@/instance.js';
|
import { isEnabledUrlPreview } from '@/instance.js';
|
||||||
import { type Keymap } from '@/scripts/hotkey.js';
|
import { type Keymap } from '@/scripts/hotkey.js';
|
||||||
|
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
@ -257,6 +257,7 @@ const renoteTime = shallowRef<HTMLElement>();
|
|||||||
const reactButton = shallowRef<HTMLElement>();
|
const reactButton = shallowRef<HTMLElement>();
|
||||||
const clipButton = shallowRef<HTMLElement>();
|
const clipButton = shallowRef<HTMLElement>();
|
||||||
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
||||||
|
const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
|
||||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
||||||
@ -318,7 +319,7 @@ const keymap = {
|
|||||||
},
|
},
|
||||||
'o': () => {
|
'o': () => {
|
||||||
if (renoteCollapsed.value) return;
|
if (renoteCollapsed.value) return;
|
||||||
showMenu();
|
galleryEl.value?.openGallery();
|
||||||
},
|
},
|
||||||
'v|enter': () => {
|
'v|enter': () => {
|
||||||
if (renoteCollapsed.value) {
|
if (renoteCollapsed.value) {
|
||||||
@ -419,7 +420,7 @@ function renote(viaKeyboard = false) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function reply(viaKeyboard = false): void {
|
function reply(): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
return;
|
return;
|
||||||
@ -427,13 +428,12 @@ function reply(viaKeyboard = false): void {
|
|||||||
os.post({
|
os.post({
|
||||||
reply: appearNote.value,
|
reply: appearNote.value,
|
||||||
channel: appearNote.value.channel,
|
channel: appearNote.value.channel,
|
||||||
animation: !viaKeyboard,
|
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
focus();
|
focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function react(viaKeyboard = false): void {
|
function react(): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||||
@ -528,18 +528,16 @@ function onContextmenu(ev: MouseEvent): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMenu(viaKeyboard = false): void {
|
function showMenu(): void {
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
|
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
|
||||||
os.popupMenu(menu, menuButton.value, {
|
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
|
||||||
viaKeyboard,
|
|
||||||
}).then(focus).finally(cleanup);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clip() {
|
async function clip(): Promise<void> {
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -547,7 +545,7 @@ async function clip() {
|
|||||||
os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
|
os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showRenoteMenu(viaKeyboard = false): void {
|
function showRenoteMenu(): void {
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -572,18 +570,14 @@ function showRenoteMenu(viaKeyboard = false): void {
|
|||||||
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
getUnrenote(),
|
getUnrenote(),
|
||||||
], renoteTime.value, {
|
], renoteTime.value);
|
||||||
viaKeyboard: viaKeyboard,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
os.popupMenu([
|
os.popupMenu([
|
||||||
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
|
getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
|
||||||
($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined,
|
($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined,
|
||||||
], renoteTime.value, {
|
], renoteTime.value);
|
||||||
viaKeyboard: viaKeyboard,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -596,11 +590,11 @@ function blur() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function focusBefore() {
|
function focusBefore() {
|
||||||
focusPrev(rootEl.value ?? null);
|
focusPrev(rootEl.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusAfter() {
|
function focusAfter() {
|
||||||
focusNext(rootEl.value ?? null);
|
focusNext(rootEl.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readPromo() {
|
function readPromo() {
|
||||||
@ -638,7 +632,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
display: block;
|
display: block;
|
||||||
@ -651,7 +645,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
width: calc(100% - 8px);
|
width: calc(100% - 8px);
|
||||||
height: calc(100% - 8px);
|
height: calc(100% - 8px);
|
||||||
border: dashed 1px var(--focus);
|
border: dashed 2px var(--focus);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
ref="rootEl"
|
ref="rootEl"
|
||||||
v-hotkey="keymap"
|
v-hotkey="keymap"
|
||||||
:class="$style.root"
|
:class="$style.root"
|
||||||
|
:tabindex="isDeleted ? '-1' : '0'"
|
||||||
>
|
>
|
||||||
<div v-if="appearNote.reply && appearNote.reply.replyId">
|
<div v-if="appearNote.reply && appearNote.reply.replyId">
|
||||||
<div v-if="!conversationLoaded" style="padding: 16px">
|
<div v-if="!conversationLoaded" style="padding: 16px">
|
||||||
@ -31,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</I18n>
|
</I18n>
|
||||||
</span>
|
</span>
|
||||||
<div :class="$style.renoteInfo">
|
<div :class="$style.renoteInfo">
|
||||||
<button ref="renoteTime" class="_button" :class="$style.renoteTime" @click="showRenoteMenu()">
|
<button ref="renoteTime" class="_button" :class="$style.renoteTime" @mousedown.prevent="showRenoteMenu()">
|
||||||
<i v-if="isMyRenote" class="ti ti-dots" style="margin-right: 4px;"></i>
|
<i v-if="isMyRenote" class="ti ti-dots" style="margin-right: 4px;"></i>
|
||||||
<MkTime :time="note.createdAt"/>
|
<MkTime :time="note.createdAt"/>
|
||||||
</button>
|
</button>
|
||||||
@ -92,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="appearNote.files && appearNote.files.length > 0">
|
<div v-if="appearNote.files && appearNote.files.length > 0">
|
||||||
<MkMediaList :mediaList="appearNote.files"/>
|
<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
|
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
|
||||||
<div v-if="isEnabledUrlPreview">
|
<div v-if="isEnabledUrlPreview">
|
||||||
@ -118,7 +119,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
ref="renoteButton"
|
ref="renoteButton"
|
||||||
class="_button"
|
class="_button"
|
||||||
:class="$style.noteFooterButton"
|
:class="$style.noteFooterButton"
|
||||||
@mousedown="renote()"
|
@mousedown.prevent="renote()"
|
||||||
>
|
>
|
||||||
<i class="ti ti-repeat"></i>
|
<i class="ti ti-repeat"></i>
|
||||||
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
||||||
@ -133,10 +134,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<i v-else class="ti ti-plus"></i>
|
<i v-else class="ti ti-plus"></i>
|
||||||
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
|
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()">
|
||||||
<i class="ti ti-paperclip"></i>
|
<i class="ti ti-paperclip"></i>
|
||||||
</button>
|
</button>
|
||||||
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="showMenu()">
|
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="showMenu()">
|
||||||
<i class="ti ti-dots"></i>
|
<i class="ti ti-dots"></i>
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
@ -281,6 +282,7 @@ const renoteTime = shallowRef<HTMLElement>();
|
|||||||
const reactButton = shallowRef<HTMLElement>();
|
const reactButton = shallowRef<HTMLElement>();
|
||||||
const clipButton = shallowRef<HTMLElement>();
|
const clipButton = shallowRef<HTMLElement>();
|
||||||
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
||||||
|
const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
|
||||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
@ -303,6 +305,7 @@ const keymap = {
|
|||||||
if (!defaultStore.state.showClipButtonInNoteFooter) return;
|
if (!defaultStore.state.showClipButtonInNoteFooter) return;
|
||||||
clip();
|
clip();
|
||||||
},
|
},
|
||||||
|
'o': () => galleryEl.value?.openGallery(),
|
||||||
'v|enter': () => {
|
'v|enter': () => {
|
||||||
if (appearNote.value.cw != null) {
|
if (appearNote.value.cw != null) {
|
||||||
showContent.value = !showContent.value;
|
showContent.value = !showContent.value;
|
||||||
@ -392,29 +395,26 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renote(viaKeyboard = false) {
|
function renote() {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
|
|
||||||
const { menu } = getRenoteMenu({ note: note.value, renoteButton });
|
const { menu } = getRenoteMenu({ note: note.value, renoteButton });
|
||||||
os.popupMenu(menu, renoteButton.value, {
|
os.popupMenu(menu, renoteButton.value);
|
||||||
viaKeyboard,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function reply(viaKeyboard = false): void {
|
function reply(): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
os.post({
|
os.post({
|
||||||
reply: appearNote.value,
|
reply: appearNote.value,
|
||||||
channel: appearNote.value.channel,
|
channel: appearNote.value.channel,
|
||||||
animation: !viaKeyboard,
|
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
focus();
|
focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function react(viaKeyboard = false): void {
|
function react(): void {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||||
@ -424,7 +424,7 @@ function react(viaKeyboard = false): void {
|
|||||||
noteId: appearNote.value.id,
|
noteId: appearNote.value.id,
|
||||||
reaction: '❤️',
|
reaction: '❤️',
|
||||||
});
|
});
|
||||||
const el = reactButton.value as HTMLElement | null | undefined;
|
const el = reactButton.value;
|
||||||
if (el) {
|
if (el) {
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
const x = rect.left + (el.offsetWidth / 2);
|
const x = rect.left + (el.offsetWidth / 2);
|
||||||
@ -488,18 +488,16 @@ function onContextmenu(ev: MouseEvent): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMenu(viaKeyboard = false): void {
|
function showMenu(): void {
|
||||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
|
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
|
||||||
os.popupMenu(menu, menuButton.value, {
|
os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
|
||||||
viaKeyboard,
|
|
||||||
}).then(focus).finally(cleanup);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clip() {
|
async function clip(): Promise<void> {
|
||||||
os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted }), clipButton.value).then(focus);
|
os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted }), clipButton.value).then(focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showRenoteMenu(viaKeyboard = false): void {
|
function showRenoteMenu(): void {
|
||||||
if (!isMyRenote) return;
|
if (!isMyRenote) return;
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
@ -512,9 +510,7 @@ function showRenoteMenu(viaKeyboard = false): void {
|
|||||||
});
|
});
|
||||||
isDeleted.value = true;
|
isDeleted.value = true;
|
||||||
},
|
},
|
||||||
}], renoteTime.value, {
|
}], renoteTime.value);
|
||||||
viaKeyboard: viaKeyboard,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function focus() {
|
function focus() {
|
||||||
@ -556,6 +552,28 @@ function loadConversation() {
|
|||||||
transition: box-shadow 0.1s ease;
|
transition: box-shadow 0.1s ease;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
contain: content;
|
contain: content;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
pointer-events: none;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
width: calc(100% - 8px);
|
||||||
|
height: calc(100% - 8px);
|
||||||
|
border: dashed 2px var(--focus);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.replyTo {
|
.replyTo {
|
||||||
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<MkAvatar :class="$style.avatar" :user="user" link preview/>
|
<MkAvatar :class="$style.avatar" :user="user"/>
|
||||||
<div :class="$style.main">
|
<div :class="$style.main">
|
||||||
<div :class="$style.header">
|
<div :class="$style.header">
|
||||||
<MkUserName :user="user" :nowrap="true"/>
|
<MkUserName :user="user" :nowrap="true"/>
|
||||||
|
@ -343,7 +343,7 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
|
|||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1">
|
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj">
|
||||||
<div v-if="page.eyeCatchingImage" class="thumbnail">
|
<div v-if="page.eyeCatchingImage" class="thumbnail">
|
||||||
<MediaImage
|
<MediaImage
|
||||||
:image="page.eyeCatchingImage"
|
:image="page.eyeCatchingImage"
|
||||||
@ -50,12 +50,29 @@ const props = defineProps<{
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.vhpxefrj {
|
.vhpxefrj {
|
||||||
display: block;
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: inset 0 0 0 2px var(--focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> .thumbnail {
|
> .thumbnail {
|
||||||
& + article {
|
& + article {
|
||||||
border-radius: 0 0 var(--radius) var(--radius);
|
border-radius: 0 0 var(--radius) var(--radius);
|
||||||
|
@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :manualShowing="manualShowing" :zPriority="'high'" :src="src" :transparentBg="true" @click="click" @close="onModalClose" @closed="onModalClosed">
|
<MkModal ref="modal" v-slot="{ type, maxHeight }" :manualShowing="manualShowing" :zPriority="'high'" :src="src" :transparentBg="true" :returnFocusTo="returnFocusTo" @click="click" @close="onModalClose" @closed="onModalClosed">
|
||||||
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="onMenuClose" @hide="hide"/>
|
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :returnFocusTo="returnFocusTo" :class="{ [$style.drawer]: type === 'drawer' }" @close="onMenuClose" @hide="hide"/>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -19,8 +19,8 @@ defineProps<{
|
|||||||
items: MenuItem[];
|
items: MenuItem[];
|
||||||
align?: 'center' | string;
|
align?: 'center' | string;
|
||||||
width?: number;
|
width?: number;
|
||||||
viaKeyboard?: boolean;
|
|
||||||
src?: any;
|
src?: any;
|
||||||
|
returnFocusTo?: HTMLElement | null;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -570,6 +570,7 @@ function clear() {
|
|||||||
|
|
||||||
function onKeydown(ev: KeyboardEvent) {
|
function onKeydown(ev: KeyboardEvent) {
|
||||||
if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post();
|
if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post();
|
||||||
|
|
||||||
if (ev.key === 'Escape') emit('esc');
|
if (ev.key === 'Escape') emit('esc');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1083,6 +1084,15 @@ defineExpose({
|
|||||||
margin: 12px 12px 12px 6px;
|
margin: 12px 12px 12px 6px;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
.submitInner {
|
||||||
|
outline: 2px solid var(--fgOnAccent);
|
||||||
|
outline-offset: -4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" :preferType="'dialog'" @click="modal?.close()" @closed="onModalClosed()">
|
<MkModal ref="modal" :preferType="'dialog'" @click="modal?.close()" @closed="onModalClosed()" @esc="modal?.close()">
|
||||||
<MkPostForm ref="form" :class="$style.form" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal?.close()" @esc="modal?.close()"/>
|
<MkPostForm ref="form" :class="$style.form" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal?.close()" @esc="modal?.close()"/>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
:class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"
|
:class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"
|
||||||
:aria-checked="checked"
|
:aria-checked="checked"
|
||||||
:aria-disabled="disabled"
|
:aria-disabled="disabled"
|
||||||
|
role="checkbox"
|
||||||
@click="toggle"
|
@click="toggle"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@ -69,6 +70,11 @@ function toggle(): void {
|
|||||||
border-color: var(--inputBorderHover) !important;
|
border-color: var(--inputBorderHover) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px var(--focus);
|
||||||
|
}
|
||||||
|
|
||||||
&.checked {
|
&.checked {
|
||||||
background-color: var(--accentedBg) !important;
|
background-color: var(--accentedBg) !important;
|
||||||
border-color: var(--accentedBg) !important;
|
border-color: var(--accentedBg) !important;
|
||||||
@ -78,7 +84,7 @@ function toggle(): void {
|
|||||||
> .button {
|
> .button {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
background-color: var(--accent);
|
background-color: var(--accent);
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@ -104,7 +110,7 @@ function toggle(): void {
|
|||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
transition: inherit;
|
transition: inherit;
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -6,20 +6,29 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div :class="$style.label" @click="focus"><slot name="label"></slot></div>
|
<div :class="$style.label" @click="focus"><slot name="label"></slot></div>
|
||||||
<div ref="container" :class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused }]" @mousedown.prevent="show">
|
<div
|
||||||
|
ref="container"
|
||||||
|
tabindex="0"
|
||||||
|
:class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused || opening }]"
|
||||||
|
@focus="focused = true"
|
||||||
|
@blur="focused = false"
|
||||||
|
@mousedown.prevent="show"
|
||||||
|
@keydown.space.enter="show"
|
||||||
|
>
|
||||||
<div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div>
|
<div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div>
|
||||||
<select
|
<select
|
||||||
ref="inputEl"
|
ref="inputEl"
|
||||||
v-model="v"
|
v-model="v"
|
||||||
v-adaptive-border
|
v-adaptive-border
|
||||||
|
tabindex="-1"
|
||||||
:class="$style.inputCore"
|
:class="$style.inputCore"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:required="required"
|
:required="required"
|
||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
@focus="focused = true"
|
|
||||||
@blur="focused = false"
|
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
|
@mousedown.prevent="() => {}"
|
||||||
|
@keydown.prevent="() => {}"
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</select>
|
</select>
|
||||||
@ -75,7 +84,7 @@ const height =
|
|||||||
props.large ? 39 :
|
props.large ? 39 :
|
||||||
36;
|
36;
|
||||||
|
|
||||||
const focus = () => inputEl.value?.focus();
|
const focus = () => container.value?.focus();
|
||||||
const onInput = (ev) => {
|
const onInput = (ev) => {
|
||||||
changed.value = true;
|
changed.value = true;
|
||||||
};
|
};
|
||||||
@ -126,7 +135,9 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
focused.value = true;
|
if (opening.value) return;
|
||||||
|
focus();
|
||||||
|
|
||||||
opening.value = true;
|
opening.value = true;
|
||||||
|
|
||||||
const menu: MenuItem[] = [];
|
const menu: MenuItem[] = [];
|
||||||
@ -173,8 +184,6 @@ function show() {
|
|||||||
onClosing: () => {
|
onClosing: () => {
|
||||||
opening.value = false;
|
opening.value = false;
|
||||||
},
|
},
|
||||||
}).then(() => {
|
|
||||||
focused.value = false;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -225,6 +234,10 @@ function show() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
> .inputCore {
|
> .inputCore {
|
||||||
border-color: var(--inputBorderHover) !important;
|
border-color: var(--inputBorderHover) !important;
|
||||||
|
@ -10,15 +10,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
<div class="items">
|
<div class="items">
|
||||||
<template v-for="(item, i) in group.items">
|
<template v-for="(item, i) in group.items">
|
||||||
<a v-if="item.type === 'a'" :href="item.href" :target="item.target" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
<a v-if="item.type === 'a'" :href="item.href" :target="item.target" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
||||||
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||||
<span class="text">{{ item.text }}</span>
|
<span class="text">{{ item.text }}</span>
|
||||||
</a>
|
</a>
|
||||||
<button v-else-if="item.type === 'button'" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)">
|
<button v-else-if="item.type === 'button'" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)">
|
||||||
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||||
<span class="text">{{ item.text }}</span>
|
<span class="text">{{ item.text }}</span>
|
||||||
</button>
|
</button>
|
||||||
<MkA v-else :to="item.to" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
<MkA v-else :to="item.to" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
||||||
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||||
<span class="text">{{ item.text }}</span>
|
<span class="text">{{ item.text }}</span>
|
||||||
</MkA>
|
</MkA>
|
||||||
@ -67,6 +67,10 @@ defineProps<{
|
|||||||
background: var(--panelHighlight);
|
background: var(--panelHighlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
background: var(--accentedBg);
|
background: var(--accentedBg);
|
||||||
|
@ -10,9 +10,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:class="$style.input"
|
:class="$style.input"
|
||||||
@keydown.enter="toggle"
|
@click="toggle"
|
||||||
>
|
>
|
||||||
<XButton :checked="checked" :disabled="disabled" @toggle="toggle"/>
|
<XButton :class="$style.toggle" :checked="checked" :disabled="disabled" @toggle="toggle"/>
|
||||||
<span v-if="!noBody" :class="$style.body">
|
<span v-if="!noBody" :class="$style.body">
|
||||||
<!-- TODO: 無名slotの方は廃止 -->
|
<!-- TODO: 無名slotの方は廃止 -->
|
||||||
<span :class="$style.label">
|
<span :class="$style.label">
|
||||||
@ -75,7 +75,13 @@ const toggle = () => {
|
|||||||
height: 0;
|
height: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
|
&:focus-visible ~ .toggle {
|
||||||
|
outline: 2px solid var(--focus);
|
||||||
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
@ -105,7 +105,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
width: calc(100% - 38px);
|
width: calc(100% - 38px);
|
||||||
|
@ -115,7 +115,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
width: calc(100% - 38px);
|
width: calc(100% - 38px);
|
||||||
|
@ -56,7 +56,7 @@ import { i18n } from '@/i18n.js';
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
width: calc(100% - 38px);
|
width: calc(100% - 38px);
|
||||||
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :src="src" @click="modal?.close()" @closed="emit('closed')">
|
<MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :src="src" @click="modal?.close()" @closed="emit('closed')" @esc="modal?.close()">
|
||||||
<div class="_popup" :class="{ [$style.root]: true, [$style.asDrawer]: type === 'drawer' }">
|
<div class="_popup" :class="{ [$style.root]: true, [$style.asDrawer]: type === 'drawer' }">
|
||||||
<div :class="[$style.label, $style.item]">
|
<div :class="[$style.label, $style.item]">
|
||||||
{{ i18n.ts.visibility }}
|
{{ i18n.ts.visibility }}
|
||||||
|
@ -8,7 +8,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<div ref="headerEl">
|
<div ref="headerEl">
|
||||||
<slot name="header"></slot>
|
<slot name="header"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div ref="bodyEl" :data-sticky-container-header-height="headerHeight">
|
<div
|
||||||
|
ref="bodyEl"
|
||||||
|
:data-sticky-container-header-height="headerHeight"
|
||||||
|
:data-sticky-container-footer-height="footerHeight"
|
||||||
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
<div ref="footerEl">
|
<div ref="footerEl">
|
||||||
|
@ -13,9 +13,9 @@ export default {
|
|||||||
el._keyHandler = makeHotkey(binding.value);
|
el._keyHandler = makeHotkey(binding.value);
|
||||||
|
|
||||||
if (el._hotkey_global) {
|
if (el._hotkey_global) {
|
||||||
document.addEventListener('keydown', el._keyHandler);
|
document.addEventListener('keydown', el._keyHandler, { passive: false });
|
||||||
} else {
|
} else {
|
||||||
el.addEventListener('keydown', el._keyHandler);
|
el.addEventListener('keydown', el._keyHandler, { passive: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
|
// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
|
||||||
|
|
||||||
import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue';
|
import { Component, markRaw, Ref, ref, defineAsyncComponent, nextTick } from 'vue';
|
||||||
import { EventEmitter } from 'eventemitter3';
|
import { EventEmitter } from 'eventemitter3';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import type { ComponentProps as CP } from 'vue-component-type-helpers';
|
import type { ComponentProps as CP } from 'vue-component-type-helpers';
|
||||||
@ -24,6 +24,8 @@ import MkContextMenu from '@/components/MkContextMenu.vue';
|
|||||||
import { MenuItem } from '@/types/menu.js';
|
import { MenuItem } from '@/types/menu.js';
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||||
|
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
|
||||||
|
import { focusParent } from './scripts/focus.js';
|
||||||
|
|
||||||
export const openingWindowsCount = ref(0);
|
export const openingWindowsCount = ref(0);
|
||||||
|
|
||||||
@ -622,31 +624,33 @@ export async function cropImage(image: Misskey.entities.DriveFile, options: {
|
|||||||
export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | null, options?: {
|
export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | null, options?: {
|
||||||
align?: string;
|
align?: string;
|
||||||
width?: number;
|
width?: number;
|
||||||
viaKeyboard?: boolean;
|
|
||||||
onClosing?: () => void;
|
onClosing?: () => void;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
return new Promise(resolve => {
|
let returnFocusTo = getHTMLElementOrNull(src) ?? getHTMLElementOrNull(document.activeElement);
|
||||||
|
return new Promise(resolve => nextTick(() => {
|
||||||
const { dispose } = popup(MkPopupMenu, {
|
const { dispose } = popup(MkPopupMenu, {
|
||||||
items,
|
items,
|
||||||
src,
|
src,
|
||||||
width: options?.width,
|
width: options?.width,
|
||||||
align: options?.align,
|
align: options?.align,
|
||||||
viaKeyboard: options?.viaKeyboard,
|
returnFocusTo,
|
||||||
}, {
|
}, {
|
||||||
closed: () => {
|
closed: () => {
|
||||||
resolve();
|
resolve();
|
||||||
dispose();
|
dispose();
|
||||||
|
returnFocusTo = null;
|
||||||
},
|
},
|
||||||
closing: () => {
|
closing: () => {
|
||||||
if (options?.onClosing) options.onClosing();
|
options?.onClosing?.();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
|
export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
|
||||||
|
let returnFocusTo = getHTMLElementOrNull(ev.currentTarget ?? ev.target) ?? getHTMLElementOrNull(document.activeElement);
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => nextTick(() => {
|
||||||
const { dispose } = popup(MkContextMenu, {
|
const { dispose } = popup(MkContextMenu, {
|
||||||
items,
|
items,
|
||||||
ev,
|
ev,
|
||||||
@ -654,14 +658,19 @@ export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
|
|||||||
closed: () => {
|
closed: () => {
|
||||||
resolve();
|
resolve();
|
||||||
dispose();
|
dispose();
|
||||||
|
|
||||||
|
// MkModalを通していないのでここでフォーカスを戻す処理を行う
|
||||||
|
if (returnFocusTo != null) {
|
||||||
|
focusParent(returnFocusTo, true, false);
|
||||||
|
returnFocusTo = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function post(props: Record<string, any> = {}): Promise<void> {
|
export function post(props: Record<string, any> = {}): Promise<void> {
|
||||||
showMovedDialog();
|
showMovedDialog();
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
|
// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
|
||||||
// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、
|
// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、
|
||||||
|
@ -234,6 +234,7 @@ onMounted(async () => {
|
|||||||
background-color: var(--accentedBg);
|
background-color: var(--accentedBg);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.danger {
|
&.danger {
|
||||||
|
@ -8,12 +8,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template #header><MkPageHeader/></template>
|
<template #header><MkPageHeader/></template>
|
||||||
<MkSpacer :contentMax="800">
|
<MkSpacer :contentMax="800">
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<div class="_panel">
|
<div class="_panel" :class="$style.link">
|
||||||
<MkA to="/bubble-game">
|
<MkA to="/bubble-game">
|
||||||
<img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
|
<img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
</div>
|
</div>
|
||||||
<div class="_panel">
|
<div class="_panel" :class="$style.link">
|
||||||
<MkA to="/reversi">
|
<MkA to="/reversi">
|
||||||
<img src="/client-assets/reversi/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
|
<img src="/client-assets/reversi/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
@ -32,3 +32,10 @@ definePageMetadata(() => ({
|
|||||||
icon: 'ti ti-device-gamepad',
|
icon: 'ti ti-device-gamepad',
|
||||||
}));
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
.link:focus-within {
|
||||||
|
outline: 2px solid var(--focus);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -286,6 +286,7 @@ definePageMetadata(() => ({
|
|||||||
background-color: var(--accentedBg);
|
background-color: var(--accentedBg);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,6 +342,7 @@ definePageMetadata(() => ({
|
|||||||
&:hover, &:focus {
|
&:hover, &:focus {
|
||||||
opacity: .7;
|
opacity: .7;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -213,12 +213,18 @@ definePageMetadata(() => ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dn:focus-visible ~ .toggle {
|
||||||
|
outline: 2px solid var(--focus);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.toggle {
|
.toggle {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
|
margin: 4px; // focus用のアウトライン
|
||||||
background-color: #83D8FF;
|
background-color: #83D8FF;
|
||||||
border-radius: 90px - 6;
|
border-radius: 90px - 6;
|
||||||
transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
|
transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
|
||||||
|
65
packages/frontend/src/scripts/focus-trap.ts
Normal file
65
packages/frontend/src/scripts/focus-trap.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
|
||||||
|
|
||||||
|
const focusTrapElements = new Set<HTMLElement>();
|
||||||
|
const ignoreElements = [
|
||||||
|
'script',
|
||||||
|
'style',
|
||||||
|
];
|
||||||
|
|
||||||
|
function containsFocusTrappedElements(el: HTMLElement): boolean {
|
||||||
|
return Array.from(focusTrapElements).some((focusTrapElement) => {
|
||||||
|
return el.contains(focusTrapElement);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function releaseFocusTrap(el: HTMLElement): void {
|
||||||
|
focusTrapElements.delete(el);
|
||||||
|
if (el.parentElement != null && el !== document.body) {
|
||||||
|
el.parentElement.childNodes.forEach((siblingNode) => {
|
||||||
|
const siblingEl = getHTMLElementOrNull(siblingNode);
|
||||||
|
if (!siblingEl) return;
|
||||||
|
if (siblingEl !== el && (focusTrapElements.has(siblingEl) || containsFocusTrappedElements(siblingEl) || focusTrapElements.size === 0)) {
|
||||||
|
siblingEl.inert = false;
|
||||||
|
} else if (
|
||||||
|
focusTrapElements.size > 0 &&
|
||||||
|
!containsFocusTrappedElements(siblingEl) &&
|
||||||
|
!focusTrapElements.has(siblingEl) &&
|
||||||
|
!ignoreElements.includes(siblingEl.tagName.toLowerCase())
|
||||||
|
) {
|
||||||
|
siblingEl.inert = true;
|
||||||
|
} else {
|
||||||
|
siblingEl.inert = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
releaseFocusTrap(el.parentElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function focusTrap(el: HTMLElement, parent: true): void;
|
||||||
|
export function focusTrap(el: HTMLElement, parent?: false): { release: () => void; };
|
||||||
|
export function focusTrap(el: HTMLElement, parent = false): { release: () => void; } | void {
|
||||||
|
if (el.parentElement != null && el !== document.body) {
|
||||||
|
el.parentElement.childNodes.forEach((siblingNode) => {
|
||||||
|
const siblingEl = getHTMLElementOrNull(siblingNode);
|
||||||
|
if (!siblingEl) return;
|
||||||
|
if (siblingEl !== el && !ignoreElements.includes(siblingEl.tagName.toLowerCase())) {
|
||||||
|
siblingEl.inert = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
focusTrap(el.parentElement, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parent) {
|
||||||
|
focusTrapElements.add(el);
|
||||||
|
|
||||||
|
return {
|
||||||
|
release: () => {
|
||||||
|
releaseFocusTrap(el);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -3,30 +3,78 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function focusPrev(el: Element | null, self = false, scroll = true) {
|
import { getScrollPosition, getScrollContainer, getStickyBottom, getStickyTop } from '@/scripts/scroll.js';
|
||||||
if (el == null) return;
|
import { getElementOrNull, getNodeOrNull } from '@/scripts/get-dom-node-or-null.js';
|
||||||
if (!self) el = el.previousElementSibling;
|
|
||||||
if (el) {
|
type MaybeHTMLElement = EventTarget | Node | Element | HTMLElement;
|
||||||
if (el.hasAttribute('tabindex')) {
|
|
||||||
(el as HTMLElement).focus({
|
export const isFocusable = (input: MaybeHTMLElement | null | undefined): input is HTMLElement => {
|
||||||
preventScroll: !scroll,
|
if (input == null || !(input instanceof HTMLElement)) return false;
|
||||||
});
|
|
||||||
|
if (input.tabIndex < 0) return false;
|
||||||
|
if ('disabled' in input && input.disabled === true) return false;
|
||||||
|
if ('readonly' in input && input.readonly === true) return false;
|
||||||
|
|
||||||
|
if (!input.ownerDocument.contains(input)) return false;
|
||||||
|
|
||||||
|
const style = window.getComputedStyle(input);
|
||||||
|
if (style.display === 'none') return false;
|
||||||
|
if (style.visibility === 'hidden') return false;
|
||||||
|
if (style.opacity === '0') return false;
|
||||||
|
if (style.pointerEvents === 'none') return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const focusPrev = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => {
|
||||||
|
const element = self ? input : getElementOrNull(input)?.previousElementSibling;
|
||||||
|
if (element == null) return;
|
||||||
|
if (isFocusable(element)) {
|
||||||
|
focusOrScroll(element, scroll);
|
||||||
} else {
|
} else {
|
||||||
focusPrev(el.previousElementSibling, true);
|
focusPrev(element, false, scroll);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const focusNext = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => {
|
||||||
|
const element = self ? input : getElementOrNull(input)?.nextElementSibling;
|
||||||
|
if (element == null) return;
|
||||||
|
if (isFocusable(element)) {
|
||||||
|
focusOrScroll(element, scroll);
|
||||||
|
} else {
|
||||||
|
focusNext(element, false, scroll);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const focusParent = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => {
|
||||||
|
const element = self ? input : getNodeOrNull(input)?.parentElement;
|
||||||
|
if (element == null) return;
|
||||||
|
if (isFocusable(element)) {
|
||||||
|
focusOrScroll(element, scroll);
|
||||||
|
} else {
|
||||||
|
focusParent(element, false, scroll);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const focusOrScroll = (element: HTMLElement, scroll: boolean) => {
|
||||||
|
if (scroll) {
|
||||||
|
const scrollContainer = getScrollContainer(element) ?? document.documentElement;
|
||||||
|
const scrollContainerTop = getScrollPosition(scrollContainer);
|
||||||
|
const stickyTop = getStickyTop(element, scrollContainer);
|
||||||
|
const stickyBottom = getStickyBottom(element, scrollContainer);
|
||||||
|
const top = element.getBoundingClientRect().top;
|
||||||
|
const bottom = element.getBoundingClientRect().bottom;
|
||||||
|
|
||||||
|
let scrollTo = scrollContainerTop;
|
||||||
|
if (top < stickyTop) {
|
||||||
|
scrollTo += top - stickyTop;
|
||||||
|
} else if (bottom > window.innerHeight - stickyBottom) {
|
||||||
|
scrollTo += bottom - window.innerHeight + stickyBottom;
|
||||||
|
}
|
||||||
|
scrollContainer.scrollTo({ top: scrollTo, behavior: 'instant' });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function focusNext(el: Element | null, self = false, scroll = true) {
|
if (document.activeElement !== element) {
|
||||||
if (el == null) return;
|
element.focus({ preventScroll: true });
|
||||||
if (!self) el = el.nextElementSibling;
|
|
||||||
if (el) {
|
|
||||||
if (el.hasAttribute('tabindex')) {
|
|
||||||
(el as HTMLElement).focus({
|
|
||||||
preventScroll: !scroll,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
focusPrev(el.nextElementSibling, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
19
packages/frontend/src/scripts/get-dom-node-or-null.ts
Normal file
19
packages/frontend/src/scripts/get-dom-node-or-null.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const getNodeOrNull = (input: unknown): Node | null => {
|
||||||
|
if (input instanceof Node) return input;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getElementOrNull = (input: unknown): Element | null => {
|
||||||
|
if (input instanceof Element) return input;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHTMLElementOrNull = (input: unknown): HTMLElement | null => {
|
||||||
|
if (input instanceof HTMLElement) return input;
|
||||||
|
return null;
|
||||||
|
};
|
@ -2,6 +2,7 @@
|
|||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
import { getHTMLElementOrNull } from "@/scripts/get-dom-node-or-null.js";
|
||||||
|
|
||||||
//#region types
|
//#region types
|
||||||
export type Keymap = Record<string, CallbackFunction | CallbackObject>;
|
export type Keymap = Record<string, CallbackFunction | CallbackObject>;
|
||||||
@ -30,8 +31,8 @@ type Action = {
|
|||||||
//#region consts
|
//#region consts
|
||||||
const KEY_ALIASES = {
|
const KEY_ALIASES = {
|
||||||
'esc': 'Escape',
|
'esc': 'Escape',
|
||||||
'enter': ['Enter', 'NumpadEnter'],
|
'enter': 'Enter',
|
||||||
'space': [' ', 'Spacebar'],
|
'space': ' ',
|
||||||
'up': 'ArrowUp',
|
'up': 'ArrowUp',
|
||||||
'down': 'ArrowDown',
|
'down': 'ArrowDown',
|
||||||
'left': 'ArrowLeft',
|
'left': 'ArrowLeft',
|
||||||
@ -44,6 +45,10 @@ const MODIFIER_KEYS = ['ctrl', 'alt', 'shift'];
|
|||||||
const IGNORE_ELEMENTS = ['input', 'textarea'];
|
const IGNORE_ELEMENTS = ['input', 'textarea'];
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
//#region store
|
||||||
|
let latestHotkey: Pattern & { callback: CallbackFunction } | null = null;
|
||||||
|
//#endregion
|
||||||
|
|
||||||
//#region impl
|
//#region impl
|
||||||
export const makeHotkey = (keymap: Keymap) => {
|
export const makeHotkey = (keymap: Keymap) => {
|
||||||
const actions = parseKeymap(keymap);
|
const actions = parseKeymap(keymap);
|
||||||
@ -51,13 +56,14 @@ export const makeHotkey = (keymap: Keymap) => {
|
|||||||
if ('pswp' in window && window.pswp != null) return;
|
if ('pswp' in window && window.pswp != null) return;
|
||||||
if (document.activeElement != null) {
|
if (document.activeElement != null) {
|
||||||
if (IGNORE_ELEMENTS.includes(document.activeElement.tagName.toLowerCase())) return;
|
if (IGNORE_ELEMENTS.includes(document.activeElement.tagName.toLowerCase())) return;
|
||||||
if ((document.activeElement as HTMLElement).isContentEditable) return;
|
if (getHTMLElementOrNull(document.activeElement)?.isContentEditable) return;
|
||||||
}
|
}
|
||||||
for (const { patterns, callback, options } of actions) {
|
for (const action of actions) {
|
||||||
if (matchPatterns(ev, patterns, options)) {
|
if (matchPatterns(ev, action)) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
callback(ev);
|
action.callback(ev);
|
||||||
|
storePattern(ev, action.callback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -102,10 +108,21 @@ const parseOptions = (rawCallback: Keymap[keyof Keymap]) => {
|
|||||||
return { ...defaultOptions } as const satisfies Action['options'];
|
return { ...defaultOptions } as const satisfies Action['options'];
|
||||||
};
|
};
|
||||||
|
|
||||||
const matchPatterns = (ev: KeyboardEvent, patterns: Action['patterns'], options: Action['options']) => {
|
const matchPatterns = (ev: KeyboardEvent, action: Action) => {
|
||||||
|
const { patterns, options, callback } = action;
|
||||||
if (ev.repeat && !options.allowRepeat) return false;
|
if (ev.repeat && !options.allowRepeat) return false;
|
||||||
const key = ev.key.toLowerCase();
|
const key = ev.key.toLowerCase();
|
||||||
return patterns.some(({ which, ctrl, shift, alt }) => {
|
return patterns.some(({ which, ctrl, shift, alt }) => {
|
||||||
|
if (
|
||||||
|
latestHotkey != null &&
|
||||||
|
latestHotkey.which.includes(key) &&
|
||||||
|
latestHotkey.ctrl === ctrl &&
|
||||||
|
latestHotkey.alt === alt &&
|
||||||
|
latestHotkey.shift === shift &&
|
||||||
|
latestHotkey.callback === callback
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!which.includes(key)) return false;
|
if (!which.includes(key)) return false;
|
||||||
if (ctrl !== (ev.ctrlKey || ev.metaKey)) return false;
|
if (ctrl !== (ev.ctrlKey || ev.metaKey)) return false;
|
||||||
if (alt !== ev.altKey) return false;
|
if (alt !== ev.altKey) return false;
|
||||||
@ -114,6 +131,26 @@ const matchPatterns = (ev: KeyboardEvent, patterns: Action['patterns'], options:
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let lastHotKeyStoreTimer: number | null = null;
|
||||||
|
|
||||||
|
const storePattern = (ev: KeyboardEvent, callback: CallbackFunction) => {
|
||||||
|
if (lastHotKeyStoreTimer != null) {
|
||||||
|
clearTimeout(lastHotKeyStoreTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
latestHotkey = {
|
||||||
|
which: [ev.key.toLowerCase()],
|
||||||
|
ctrl: ev.ctrlKey || ev.metaKey,
|
||||||
|
alt: ev.altKey,
|
||||||
|
shift: ev.shiftKey,
|
||||||
|
callback,
|
||||||
|
};
|
||||||
|
|
||||||
|
lastHotKeyStoreTimer = window.setTimeout(() => {
|
||||||
|
latestHotkey = null;
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
const parseKeyCode = (input?: string | null) => {
|
const parseKeyCode = (input?: string | null) => {
|
||||||
if (input == null) return [];
|
if (input == null) return [];
|
||||||
const raw = getValueByKey(KEY_ALIASES, input);
|
const raw = getValueByKey(KEY_ALIASES, input);
|
||||||
|
@ -23,6 +23,14 @@ export function getStickyTop(el: HTMLElement, container: HTMLElement | null = nu
|
|||||||
return getStickyTop(el.parentElement, container, newTop);
|
return getStickyTop(el.parentElement, container, newTop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getStickyBottom(el: HTMLElement, container: HTMLElement | null = null, bottom = 0) {
|
||||||
|
if (!el.parentElement) return bottom;
|
||||||
|
const data = el.dataset.stickyContainerFooterHeight;
|
||||||
|
const newBottom = data ? Number(data) + bottom : bottom;
|
||||||
|
if (el === container) return newBottom;
|
||||||
|
return getStickyBottom(el.parentElement, container, newBottom);
|
||||||
|
}
|
||||||
|
|
||||||
export function getScrollPosition(el: HTMLElement | null): number {
|
export function getScrollPosition(el: HTMLElement | null): number {
|
||||||
const container = getScrollContainer(el);
|
const container = getScrollContainer(el);
|
||||||
return container == null ? window.scrollY : container.scrollTop;
|
return container == null ? window.scrollY : container.scrollTop;
|
||||||
|
@ -113,6 +113,10 @@ a {
|
|||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
-webkit-touch-callout: none;
|
-webkit-touch-callout: none;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
@ -143,12 +147,21 @@ rt {
|
|||||||
white-space: initial;
|
white-space: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:focus-visible {
|
||||||
|
outline: var(--focus) solid 2px;
|
||||||
|
outline-offset: -2px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ti {
|
.ti {
|
||||||
width: 1.28em;
|
width: 1.28em;
|
||||||
vertical-align: -12%;
|
vertical-align: -12%;
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
font-size: 128%;
|
font-size: 128%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -230,10 +243,6 @@ rt {
|
|||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
@ -270,13 +279,17 @@ rt {
|
|||||||
|
|
||||||
._help {
|
._help {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
cursor: help
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
._textButton {
|
._textButton {
|
||||||
@extend ._button;
|
@extend ._button;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
&:not(:disabled):hover {
|
&:not(:disabled):hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
@ -227,7 +227,7 @@ if ($i) {
|
|||||||
right: 15px;
|
right: 15px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
|
@ -139,7 +139,7 @@ function more() {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
width: calc(100% - 38px);
|
width: calc(100% - 38px);
|
||||||
@ -155,7 +155,7 @@ function more() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover, &.active {
|
&:hover, &.active {
|
||||||
&:before {
|
&::before {
|
||||||
background: var(--accentLighten);
|
background: var(--accentLighten);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,7 +226,7 @@ function more() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover, &.active {
|
&:hover, &.active {
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
width: calc(100% - 24px);
|
width: calc(100% - 24px);
|
||||||
|
@ -166,6 +166,15 @@ function more(ev: MouseEvent) {
|
|||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
> .instanceIcon {
|
||||||
|
outline: 2px solid var(--focus);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.instanceIcon {
|
.instanceIcon {
|
||||||
@ -192,7 +201,7 @@ function more(ev: MouseEvent) {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
width: calc(100% - 38px);
|
width: calc(100% - 38px);
|
||||||
@ -207,8 +216,17 @@ function more(ev: MouseEvent) {
|
|||||||
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
outline: 2px solid var(--fgOnAccent);
|
||||||
|
outline-offset: -4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover, &.active {
|
&:hover, &.active {
|
||||||
&:before {
|
&::before {
|
||||||
background: var(--accentLighten);
|
background: var(--accentLighten);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -234,6 +252,14 @@ function more(ev: MouseEvent) {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
> .avatar {
|
||||||
|
box-shadow: 0 0 0 4px var(--focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
@ -282,10 +308,19 @@ function more(ev: MouseEvent) {
|
|||||||
color: var(--navActive);
|
color: var(--navActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover, &.active {
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
outline: 2px solid var(--focus);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active, &:focus {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
width: calc(100% - 34px);
|
width: calc(100% - 34px);
|
||||||
@ -352,6 +387,15 @@ function more(ev: MouseEvent) {
|
|||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
> .instanceIcon {
|
||||||
|
outline: 2px solid var(--focus);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.instanceIcon {
|
.instanceIcon {
|
||||||
@ -376,7 +420,7 @@ function more(ev: MouseEvent) {
|
|||||||
height: 52px;
|
height: 52px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -391,8 +435,17 @@ function more(ev: MouseEvent) {
|
|||||||
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
outline: 2px solid var(--fgOnAccent);
|
||||||
|
outline-offset: -4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover, &.active {
|
&:hover, &.active {
|
||||||
&:before {
|
&::before {
|
||||||
background: var(--accentLighten);
|
background: var(--accentLighten);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -413,6 +466,14 @@ function more(ev: MouseEvent) {
|
|||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
> .avatar {
|
||||||
|
box-shadow: 0 0 0 4px var(--focus);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
@ -442,11 +503,20 @@ function more(ev: MouseEvent) {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
&:hover, &.active {
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
outline: 2px solid var(--focus);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active, &:focus {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -271,7 +271,7 @@ function onDrop(ev) {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|
||||||
&.draghover {
|
&.draghover {
|
||||||
&:after {
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -285,7 +285,7 @@ function onDrop(ev) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.dragging {
|
&.dragging {
|
||||||
&:after {
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -121,7 +121,7 @@ defineExpose<WidgetComponentExpose>({
|
|||||||
.root {
|
.root {
|
||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
clear: both;
|
clear: both;
|
||||||
|
Loading…
Reference in New Issue
Block a user