diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue index b98e20bad6e9b0a7d868daa90c11045b2f40e735..b5b23386e71458f0836632fb78625d6fdbf09727 100644 --- a/packages/frontend/src/components/SkNote.vue +++ b/packages/frontend/src/components/SkNote.vue @@ -776,6 +776,10 @@ function focusAfter() { focusNext(el.value); } +function scrollIntoView() { + el.value.scrollIntoView(); +} + function readPromo() { os.api('promo/read', { noteId: appearNote.value.id, @@ -790,6 +794,12 @@ function emitUpdReaction(emoji: string, delta: number) { emit('reaction', emoji); } } + +defineExpose({ + focus, + blur, + scrollIntoView, +}); </script> <style lang="scss" module> @@ -824,7 +834,7 @@ function emitUpdReaction(emoji: string, delta: number) { margin: auto; width: calc(100% - 8px); height: calc(100% - 8px); - border: dashed 1px var(--focus); + border: solid 1px var(--focus); border-radius: var(--radius); box-sizing: border-box; } @@ -894,7 +904,7 @@ function emitUpdReaction(emoji: string, delta: number) { position: relative; display: flex; align-items: center; - padding: 24px 32px 16px calc(32px + var(--avatar) + 14px); + padding: 24px 32px 0 calc(32px + var(--avatar) + 14px); line-height: 28px; white-space: pre; color: var(--renote); diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue index ca3e8ca3f776df44b4997e36dc9a27130d93d3bd..4a06e8f56ab2ad00c67ca31a29dc2ed8e0eeef9f 100644 --- a/packages/frontend/src/components/SkNoteDetailed.vue +++ b/packages/frontend/src/components/SkNoteDetailed.vue @@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only <SkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note" :expandAllCws="props.expandAllCws"/> </template> <SkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo" :expandAllCws="props.expandAllCws"/> - <article :class="$style.note" @contextmenu.stop="onContextmenu"> + <article :id="appearNote.id" ref="noteEl" :class="$style.note" tabindex="-1" @contextmenu.stop="onContextmenu"> <header :class="$style.noteHeader"> <MkAvatar :class="$style.noteHeaderAvatar" :user="appearNote.user" indicator link preview/> <div style="display: flex; align-items: center; white-space: nowrap; overflow: hidden;"> @@ -228,7 +228,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, inject, onMounted, provide, ref, shallowRef, watch } from 'vue'; +import { computed, inject, onMounted, onUnmounted, onUpdated, provide, ref, shallowRef, watch } from 'vue'; import * as mfm from '@sharkey/sfm-js'; import * as Misskey from 'misskey-js'; import SkNoteSub from '@/components/SkNoteSub.vue'; @@ -301,6 +301,7 @@ const isRenote = ( ); const el = shallowRef<HTMLElement>(); +const noteEl = shallowRef<HTMLElement>(); const menuButton = shallowRef<HTMLElement>(); const menuVersionsButton = shallowRef<HTMLElement>(); const renoteButton = shallowRef<HTMLElement>(); @@ -731,11 +732,11 @@ function showRenoteMenu(viaKeyboard = false): void { } function focus() { - el.value.focus(); + noteEl.value?.focus(); } function blur() { - el.value.blur(); + noteEl.value?.blur(); } const repliesLoaded = ref(false); @@ -776,6 +777,7 @@ function loadConversation() { noteId: appearNote.value.replyId, }).then(res => { conversation.value = res.reverse(); + focus(); }); } @@ -792,6 +794,31 @@ function animatedMFM() { }).then((res) => { if (!res.canceled) allowAnim.value = true; }); } } + +let isScrolling = false; + +function setScrolling() { + isScrolling = true; +} + +onMounted(() => { + document.addEventListener('wheel', setScrolling); + isScrolling = false; + noteEl.value?.scrollIntoView({ block: 'center' }); +}); + +onUpdated(() => { + if (!isScrolling) { + noteEl.value?.scrollIntoView({ block: 'center' }); + if (location.hash) { + location.replace(location.hash); // Jump to highlighted reply + } + } +}); + +onUnmounted(() => { + document.removeEventListener('wheel', setScrolling); +}); </script> <style lang="scss" module> @@ -863,6 +890,7 @@ function animatedMFM() { } .note { + position: relative; padding: 32px; font-size: 1.2em; overflow: hidden; @@ -870,6 +898,28 @@ function animatedMFM() { &:hover > .main > .footer > .button { opacity: 1; } + + &: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: solid 1px var(--focus); + border-radius: var(--radius); + box-sizing: border-box; + } + } } .noteHeader { diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue index 79e171f34c47d3eb94a9ef507aa0af12cca340b1..8b3ced37618a3e1ae0af0f34ef3be1a9b71e0e07 100644 --- a/packages/frontend/src/components/SkNoteSub.vue +++ b/packages/frontend/src/components/SkNoteSub.vue @@ -461,7 +461,27 @@ if (props.detail) { } .main { - display: flex; + position: relative; + display: flex; + + &::after { + content: ""; + position: absolute; + top: -12px; + right: -12px; + left: -12px; + bottom: -12px; + background: var(--panelHighlight); + border-radius: var(--radius); + opacity: 0; + transition: opacity .2s, background .2s; + z-index: -1; + } + + &:hover::after, + &:focus-within::after { + opacity: 1; + } } .colorBar { diff --git a/packages/frontend/src/nirax.ts b/packages/frontend/src/nirax.ts index 9755bdcb18fefb559c672be258e6a4aaa2874ca4..9564a754f03ef89552c79ea9c9a569447ff3a1ca 100644 --- a/packages/frontend/src/nirax.ts +++ b/packages/frontend/src/nirax.ts @@ -78,7 +78,7 @@ export class Router extends EventEmitter<{ public current: Resolved; public currentRef: ShallowRef<Resolved> = shallowRef(); public currentRoute: ShallowRef<RouteDef> = shallowRef(); - private currentPath: string; + private currentPath = ''; private isLoggedIn: boolean; private notFoundPageComponent: Component; private currentKey = Date.now().toString(); @@ -89,7 +89,7 @@ export class Router extends EventEmitter<{ super(); this.routes = routes; - this.currentPath = currentPath; + //this.currentPath = currentPath; this.isLoggedIn = isLoggedIn; this.notFoundPageComponent = notFoundPageComponent; this.navigate(currentPath, null, false); diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index b861afa9a37fbba13513fe18e31028bc61e2dd2e..f12f7bde79241dcbfe2bbfc165f3b3a14ab6188a 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -545,12 +545,48 @@ export const mainRouter = new Router(routes, location.pathname + location.search window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href); +const scrollPosStore = new Map<string, number>(); +let restoring = false; + +window.setInterval(() => { + if (!restoring) { + scrollPosStore.set(window.history.state?.key, window.scrollY); + } +}, 1000); + mainRouter.addListener('push', ctx => { window.history.pushState({ key: ctx.key }, '', ctx.path); + + restoring = true; + const scrollPos = scrollPosStore.get(ctx.key) ?? 0; + window.scroll({ top: scrollPos, behavior: 'instant' }); + + if (scrollPos !== 0) { + window.setTimeout(() => { + // é·ç§»ç›´å¾Œã¯ã‚¿ã‚¤ãƒŸãƒ³ã‚°ã«ã‚ˆã£ã¦ã¯ã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆãŒå¾©å…ƒã—切ã£ã¦ãªã„å¯èƒ½æ€§ã‚‚考ãˆã‚‰ã‚Œã‚‹ãŸã‚å°‘ã—時間を空ã‘ã¦å†åº¦ã‚¹ã‚¯ãƒãƒ¼ãƒ« + window.scroll({ top: scrollPos, behavior: 'instant' }); + }, 100); + restoring = false; + } else { + restoring = false; + } +}); + +mainRouter.addListener('same', () => { + window.scroll({ top: 0, behavior: 'smooth' }); }); window.addEventListener('popstate', (event) => { mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key); + + restoring = true; + const scrollPos = scrollPosStore.get(event.state?.key) ?? 0; + window.scroll({ top: scrollPos, behavior: 'instant' }); + window.setTimeout(() => { + // é·ç§»ç›´å¾Œã¯ã‚¿ã‚¤ãƒŸãƒ³ã‚°ã«ã‚ˆã£ã¦ã¯ã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆãŒå¾©å…ƒã—切ã£ã¦ãªã„å¯èƒ½æ€§ã‚‚考ãˆã‚‰ã‚Œã‚‹ãŸã‚å°‘ã—時間を空ã‘ã¦å†åº¦ã‚¹ã‚¯ãƒãƒ¼ãƒ« + window.scroll({ top: scrollPos, behavior: 'instant' }); + restoring = false; + }, 100); }); export function useRouter(): Router {