From 912d523a9bbcb0930e255c0d62e4dda6ddee4c02 Mon Sep 17 00:00:00 2001 From: TAKAHASHI Shuuji Date: Mon, 25 Mar 2024 03:09:19 +0900 Subject: [PATCH 1/7] feat: keyboard status navigation with j/k --- plugins/magic-keys.client.ts | 38 ++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/plugins/magic-keys.client.ts b/plugins/magic-keys.client.ts index 2c885347..10ab78c0 100644 --- a/plugins/magic-keys.client.ts +++ b/plugins/magic-keys.client.ts @@ -76,4 +76,42 @@ export default defineNuxtPlugin(({ $scrollToTop }) => { ?.click() } whenever(logicAnd(isAuthenticated, notUsingInput, keys['.']), showNewItems) + + const statusSelector = '[aria-roledescription="status-card"]' + + // find the nearest status element id traversing up from the current active element + // `activeElement` can be some of an element within a status element + // otherwise, reach to the root `` + function getActiveStatueId(element: HTMLElement): string | undefined { + if (element.nodeName === 'HTML') + return undefined + + if (element.matches(statusSelector)) + return element.id + + return getActiveStatueId(element.parentNode as HTMLElement) + } + + function focusNextOrPreviousStatus(direction: 'next' | 'previous') { + const activeStatusId = getActiveStatueId(activeElement.value) + if (activeStatusId) { + const nextOrPreviousStatusId = getNextOrPreviousStatusId(activeStatusId, direction) + if (nextOrPreviousStatusId) { + document.getElementById(nextOrPreviousStatusId)?.scrollIntoView({ behavior: 'smooth', block: 'center' }) + document.getElementById(nextOrPreviousStatusId)?.focus() + } + } + } + + function getNextOrPreviousStatusId(currentStatusId: string, direction: 'next' | 'previous'): string | undefined { + const statusIds = [...document.querySelectorAll(statusSelector)].map(s => s.id) + const currentIndex = statusIds.findIndex(id => id === currentStatusId) + const statusId = direction === 'next' + ? statusIds[Math.min(currentIndex + 1, statusIds.length)] + : statusIds[Math.max(0, currentIndex - 1)] + return statusId + } + + whenever(logicAnd(notUsingInput, keys.j), () => focusNextOrPreviousStatus('next')) + whenever(logicAnd(notUsingInput, keys.k), () => focusNextOrPreviousStatus('previous')) }) From a988b79574673a31aaac71453e014a1dd92db5fb Mon Sep 17 00:00:00 2001 From: TAKAHASHI Shuuji Date: Sun, 7 Apr 2024 22:39:35 +0900 Subject: [PATCH 2/7] fix: always focus on first status when no selection --- plugins/magic-keys.client.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/plugins/magic-keys.client.ts b/plugins/magic-keys.client.ts index 10ab78c0..150a5cc2 100644 --- a/plugins/magic-keys.client.ts +++ b/plugins/magic-keys.client.ts @@ -93,18 +93,21 @@ export default defineNuxtPlugin(({ $scrollToTop }) => { } function focusNextOrPreviousStatus(direction: 'next' | 'previous') { - const activeStatusId = getActiveStatueId(activeElement.value) - if (activeStatusId) { - const nextOrPreviousStatusId = getNextOrPreviousStatusId(activeStatusId, direction) - if (nextOrPreviousStatusId) { - document.getElementById(nextOrPreviousStatusId)?.scrollIntoView({ behavior: 'smooth', block: 'center' }) - document.getElementById(nextOrPreviousStatusId)?.focus() - } + const activeStatusId = activeElement.value ? getActiveStatueId(activeElement.value) : undefined + const nextOrPreviousStatusId = getNextOrPreviousStatusId(activeStatusId, direction) + if (nextOrPreviousStatusId) { + document.getElementById(nextOrPreviousStatusId)?.scrollIntoView({ behavior: 'smooth', block: 'center' }) + document.getElementById(nextOrPreviousStatusId)?.focus() } } - function getNextOrPreviousStatusId(currentStatusId: string, direction: 'next' | 'previous'): string | undefined { + function getNextOrPreviousStatusId(currentStatusId: string | undefined, direction: 'next' | 'previous'): string | undefined { const statusIds = [...document.querySelectorAll(statusSelector)].map(s => s.id) + if (currentStatusId === undefined) { + // if there is no selection, always focus on the first status + return statusIds[0] + } + const currentIndex = statusIds.findIndex(id => id === currentStatusId) const statusId = direction === 'next' ? statusIds[Math.min(currentIndex + 1, statusIds.length)] From f4c287481cb27d8e3b08cf705382b4d5d600d4a5 Mon Sep 17 00:00:00 2001 From: TAKAHASHI Shuuji Date: Sun, 7 Apr 2024 23:57:10 +0900 Subject: [PATCH 3/7] fix: scroll to just below title bar consistently --- plugins/magic-keys.client.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/magic-keys.client.ts b/plugins/magic-keys.client.ts index 150a5cc2..d9eb33a3 100644 --- a/plugins/magic-keys.client.ts +++ b/plugins/magic-keys.client.ts @@ -6,6 +6,7 @@ export default defineNuxtPlugin(({ $scrollToTop }) => { const keys = useMagicKeys() const router = useRouter() const i18n = useNuxtApp().$i18n + const { y } = useWindowScroll({ behavior: 'smooth' }) // disable shortcuts when focused on inputs (https://vueuse.org/core/usemagickeys/#conditionally-disable) const activeElement = useActiveElement() @@ -96,8 +97,12 @@ export default defineNuxtPlugin(({ $scrollToTop }) => { const activeStatusId = activeElement.value ? getActiveStatueId(activeElement.value) : undefined const nextOrPreviousStatusId = getNextOrPreviousStatusId(activeStatusId, direction) if (nextOrPreviousStatusId) { - document.getElementById(nextOrPreviousStatusId)?.scrollIntoView({ behavior: 'smooth', block: 'center' }) - document.getElementById(nextOrPreviousStatusId)?.focus() + const status = document.getElementById(nextOrPreviousStatusId) + if (status) { + status.focus({ preventScroll: true }) + const topBarHeight = 58 + y.value += status.getBoundingClientRect().top - topBarHeight + } } } From 9d327f17aaf0c9a419ac13cffccd6c1ef8f9c778 Mon Sep 17 00:00:00 2001 From: TAKAHASHI Shuuji Date: Sun, 21 Apr 2024 21:12:46 +0900 Subject: [PATCH 4/7] fix: disable smooth scrolling --- plugins/magic-keys.client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/magic-keys.client.ts b/plugins/magic-keys.client.ts index d9eb33a3..1e1e3430 100644 --- a/plugins/magic-keys.client.ts +++ b/plugins/magic-keys.client.ts @@ -6,7 +6,7 @@ export default defineNuxtPlugin(({ $scrollToTop }) => { const keys = useMagicKeys() const router = useRouter() const i18n = useNuxtApp().$i18n - const { y } = useWindowScroll({ behavior: 'smooth' }) + const { y } = useWindowScroll({ behavior: 'instant' }) // disable shortcuts when focused on inputs (https://vueuse.org/core/usemagickeys/#conditionally-disable) const activeElement = useActiveElement() From 06f8bb83887490aeac0f39688406bcd9ab41d063 Mon Sep 17 00:00:00 2001 From: TAKAHASHI Shuuji Date: Mon, 22 Apr 2024 13:04:12 +0900 Subject: [PATCH 5/7] fix: disable j/k keyboard shortcut when enabled virtual scroller --- plugins/magic-keys.client.ts | 74 +++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/plugins/magic-keys.client.ts b/plugins/magic-keys.client.ts index 1e1e3430..84128344 100644 --- a/plugins/magic-keys.client.ts +++ b/plugins/magic-keys.client.ts @@ -7,6 +7,7 @@ export default defineNuxtPlugin(({ $scrollToTop }) => { const router = useRouter() const i18n = useNuxtApp().$i18n const { y } = useWindowScroll({ behavior: 'instant' }) + const virtualScroller = usePreferences('experimentalVirtualScroller') // disable shortcuts when focused on inputs (https://vueuse.org/core/usemagickeys/#conditionally-disable) const activeElement = useActiveElement() @@ -78,48 +79,53 @@ export default defineNuxtPlugin(({ $scrollToTop }) => { } whenever(logicAnd(isAuthenticated, notUsingInput, keys['.']), showNewItems) - const statusSelector = '[aria-roledescription="status-card"]' + // TODO: virtual scroller cannot load off-screen post + // that prevents focusing next post properly + // we disabled this shortcut when enabled virtual scroller + if (!virtualScroller.value) { + const statusSelector = '[aria-roledescription="status-card"]' - // find the nearest status element id traversing up from the current active element - // `activeElement` can be some of an element within a status element - // otherwise, reach to the root `` - function getActiveStatueId(element: HTMLElement): string | undefined { - if (element.nodeName === 'HTML') - return undefined + // find the nearest status element id traversing up from the current active element + // `activeElement` can be some of an element within a status element + // otherwise, reach to the root `` + function getActiveStatueId(element: HTMLElement): string | undefined { + if (element.nodeName === 'HTML') + return undefined - if (element.matches(statusSelector)) - return element.id + if (element.matches(statusSelector)) + return element.id - return getActiveStatueId(element.parentNode as HTMLElement) - } + return getActiveStatueId(element.parentNode as HTMLElement) + } - function focusNextOrPreviousStatus(direction: 'next' | 'previous') { - const activeStatusId = activeElement.value ? getActiveStatueId(activeElement.value) : undefined - const nextOrPreviousStatusId = getNextOrPreviousStatusId(activeStatusId, direction) - if (nextOrPreviousStatusId) { - const status = document.getElementById(nextOrPreviousStatusId) - if (status) { - status.focus({ preventScroll: true }) - const topBarHeight = 58 - y.value += status.getBoundingClientRect().top - topBarHeight + function focusNextOrPreviousStatus(direction: 'next' | 'previous') { + const activeStatusId = activeElement.value ? getActiveStatueId(activeElement.value) : undefined + const nextOrPreviousStatusId = getNextOrPreviousStatusId(activeStatusId, direction) + if (nextOrPreviousStatusId) { + const status = document.getElementById(nextOrPreviousStatusId) + if (status) { + status.focus({ preventScroll: true }) + const topBarHeight = 58 + y.value += status.getBoundingClientRect().top - topBarHeight + } } } - } - function getNextOrPreviousStatusId(currentStatusId: string | undefined, direction: 'next' | 'previous'): string | undefined { - const statusIds = [...document.querySelectorAll(statusSelector)].map(s => s.id) - if (currentStatusId === undefined) { - // if there is no selection, always focus on the first status - return statusIds[0] + function getNextOrPreviousStatusId(currentStatusId: string | undefined, direction: 'next' | 'previous'): string | undefined { + const statusIds = [...document.querySelectorAll(statusSelector)].map(s => s.id) + if (currentStatusId === undefined) { + // if there is no selection, always focus on the first status + return statusIds[0] + } + + const currentIndex = statusIds.findIndex(id => id === currentStatusId) + const statusId = direction === 'next' + ? statusIds[Math.min(currentIndex + 1, statusIds.length)] + : statusIds[Math.max(0, currentIndex - 1)] + return statusId } - const currentIndex = statusIds.findIndex(id => id === currentStatusId) - const statusId = direction === 'next' - ? statusIds[Math.min(currentIndex + 1, statusIds.length)] - : statusIds[Math.max(0, currentIndex - 1)] - return statusId + whenever(logicAnd(notUsingInput, keys.j), () => focusNextOrPreviousStatus('next')) + whenever(logicAnd(notUsingInput, keys.k), () => focusNextOrPreviousStatus('previous')) } - - whenever(logicAnd(notUsingInput, keys.j), () => focusNextOrPreviousStatus('next')) - whenever(logicAnd(notUsingInput, keys.k), () => focusNextOrPreviousStatus('previous')) }) From facc9a8f13bb5940e1bb625d224a85df95a8d4ff Mon Sep 17 00:00:00 2001 From: TAKAHASHI Shuuji Date: Mon, 22 Apr 2024 13:06:50 +0900 Subject: [PATCH 6/7] feat: revive keyboard shortcut help for j/k --- .../magickeys/MagickeysKeyboardShortcuts.vue | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/components/magickeys/MagickeysKeyboardShortcuts.vue b/components/magickeys/MagickeysKeyboardShortcuts.vue index c6f5d308..e5b1f4c9 100644 --- a/components/magickeys/MagickeysKeyboardShortcuts.vue +++ b/components/magickeys/MagickeysKeyboardShortcuts.vue @@ -32,14 +32,14 @@ const shortcutItemGroups = computed(() => [ description: t('magic_keys.groups.navigation.shortcut_help'), shortcut: { keys: ['?'], isSequence: false }, }, - // { - // description: t('magic_keys.groups.navigation.next_status'), - // shortcut: { keys: ['j'], isSequence: false }, - // }, - // { - // description: t('magic_keys.groups.navigation.previous_status'), - // shortcut: { keys: ['k'], isSequence: false }, - // }, + { + description: t('magic_keys.groups.navigation.next_status'), + shortcut: { keys: ['j'], isSequence: false }, + }, + { + description: t('magic_keys.groups.navigation.previous_status'), + shortcut: { keys: ['k'], isSequence: false }, + }, { description: t('magic_keys.groups.navigation.go_to_search'), shortcut: { keys: ['/'], isSequence: false }, From aa171a82740aab8f7c93946f86789e6b289521f5 Mon Sep 17 00:00:00 2001 From: TAKAHASHI Shuuji Date: Mon, 22 Apr 2024 13:26:36 +0900 Subject: [PATCH 7/7] feat: add instruction to disable "Virtual Scrolling" setting in shortcut help --- components/magickeys/MagickeysKeyboardShortcuts.vue | 8 +++++++- locales/en.json | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/components/magickeys/MagickeysKeyboardShortcuts.vue b/components/magickeys/MagickeysKeyboardShortcuts.vue index e5b1f4c9..e0c80ab1 100644 --- a/components/magickeys/MagickeysKeyboardShortcuts.vue +++ b/components/magickeys/MagickeysKeyboardShortcuts.vue @@ -14,6 +14,7 @@ interface ShortcutDef { interface ShortcutItem { description: string shortcut: ShortcutDef + info?: string } interface ShortcutItemGroup { @@ -35,10 +36,12 @@ const shortcutItemGroups = computed(() => [ { description: t('magic_keys.groups.navigation.next_status'), shortcut: { keys: ['j'], isSequence: false }, + info: t('magic_keys.info.disable_virtual_scrolling'), }, { description: t('magic_keys.groups.navigation.previous_status'), shortcut: { keys: ['k'], isSequence: false }, + info: t('magic_keys.info.disable_virtual_scrolling'), }, { description: t('magic_keys.groups.navigation.go_to_search'), @@ -147,8 +150,11 @@ const shortcutItemGroups = computed(() => [ :key="item.description" flex my-1 lg:my-2 justify-between place-items-center max-w-full text-base > -
+
{{ item.description }} + +
+