Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: keyboard status navigation with j/k #2739

Merged
merged 8 commits into from
May 31, 2024
16 changes: 8 additions & 8 deletions components/magickeys/MagickeysKeyboardShortcuts.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ const shortcutItemGroups = computed<ShortcutItemGroup[]>(() => [
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 },
Expand Down
52 changes: 52 additions & 0 deletions plugins/magic-keys.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export default defineNuxtPlugin(({ $scrollToTop }) => {
const keys = useMagicKeys()
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()
Expand Down Expand Up @@ -76,4 +78,54 @@ export default defineNuxtPlugin(({ $scrollToTop }) => {
?.click()
}
whenever(logicAnd(isAuthenticated, notUsingInput, keys['.']), showNewItems)

// 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 `<html>`
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 = 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]
}

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'))
}
})