Skip to content

Commit 41f16b5

Browse files
alex-keydanielroe
andauthored
feat: keyboard shortcuts switcher in Settings (#1684)
Co-authored-by: Daniel Roe <daniel@roe.dev>
1 parent 5e3b034 commit 41f16b5

File tree

16 files changed

+214
-29
lines changed

16 files changed

+214
-29
lines changed

app/app.vue

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@ if (import.meta.server) {
4747
setJsonLd(createWebSiteSchema())
4848
}
4949
50+
const keyboardShortcuts = useKeyboardShortcuts()
51+
5052
onKeyDown(
5153
'/',
5254
e => {
53-
if (isEditableElement(e.target)) return
55+
if (!keyboardShortcuts.value || isEditableElement(e.target)) return
5456
e.preventDefault()
5557
5658
const searchInput = document.querySelector<HTMLInputElement>(
@@ -70,7 +72,7 @@ onKeyDown(
7072
onKeyDown(
7173
'?',
7274
e => {
73-
if (isEditableElement(e.target)) return
75+
if (!keyboardShortcuts.value || isEditableElement(e.target)) return
7476
e.preventDefault()
7577
showKbdHints.value = true
7678
},
@@ -80,7 +82,7 @@ onKeyDown(
8082
onKeyUp(
8183
'?',
8284
e => {
83-
if (isEditableElement(e.target)) return
85+
if (!keyboardShortcuts.value || isEditableElement(e.target)) return
8486
e.preventDefault()
8587
showKbdHints.value = false
8688
},

app/assets/main.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,11 @@ input[type='search']::-webkit-search-results-decoration {
335335
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
336336
}
337337

338+
/* Hide keyboard shortcut hints before hydration when user disabled them */
339+
:root[data-kbd-shortcuts='false'] [data-kbd-hint] {
340+
display: none;
341+
}
342+
338343
/* Locking the scroll whenever any of the modals are open */
339344
html:has(dialog:modal) {
340345
overflow: hidden;

app/components/AppFooter.vue

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const isHome = computed(() => route.name === 'index')
66
77
const modalRef = useTemplateRef('modalRef')
88
const showModal = () => modalRef.value?.showModal?.()
9+
const closeModal = () => modalRef.value?.close?.()
910
</script>
1011

1112
<template>
@@ -81,7 +82,7 @@ const showModal = () => modalRef.value?.showModal?.()
8182
<p class="mb-2 font-mono text-fg-subtle">
8283
{{ $t('shortcuts.section.package') }}
8384
</p>
84-
<ul class="mb-6 flex flex-col gap-2">
85+
<ul class="mb-8 flex flex-col gap-2">
8586
<li class="flex gap-2 items-center">
8687
<kbd class="kbd">.</kbd>
8788
<span>{{ $t('shortcuts.open_code_view') }}</span>
@@ -95,6 +96,19 @@ const showModal = () => modalRef.value?.showModal?.()
9596
<span>{{ $t('shortcuts.compare_from_package') }}</span>
9697
</li>
9798
</ul>
99+
<p class="text-fg-muted leading-relaxed">
100+
<i18n-t keypath="shortcuts.disable_shortcuts" tag="span" scope="global">
101+
<template #settings>
102+
<NuxtLink
103+
:to="{ name: 'settings' }"
104+
class="hover:text-fg underline decoration-fg-subtle/50 hover:decoration-fg"
105+
@click="closeModal"
106+
>
107+
{{ $t('settings.title') }}
108+
</NuxtLink>
109+
</template>
110+
</i18n-t>
111+
</p>
98112
</Modal>
99113
<LinkBase :to="NPMX_DOCS_SITE">
100114
{{ $t('footer.docs') }}

app/components/AppHeader.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import type { NavigationConfig, NavigationConfigWithGroups } from '~/types'
44
import { isEditableElement } from '~/utils/input'
55
import { NPMX_DOCS_SITE } from '#shared/utils/constants'
66
7+
const keyboardShortcuts = useKeyboardShortcuts()
8+
79
withDefaults(
810
defineProps<{
911
showLogo?: boolean
@@ -175,7 +177,7 @@ function handleSearchFocus() {
175177
176178
onKeyStroke(
177179
e => {
178-
if (isEditableElement(e.target)) {
180+
if (!keyboardShortcuts.value || isEditableElement(e.target)) {
179181
return
180182
}
181183

app/components/Button/Base.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ const props = withDefaults(
2626
2727
const el = useTemplateRef('el')
2828
29+
const keyboardShortcutsEnabled = useKeyboardShortcuts()
30+
2931
defineExpose({
3032
focus: () => el.value?.focus(),
3133
getBoundingClientRect: () => el.value?.getBoundingClientRect(),
@@ -56,12 +58,13 @@ defineExpose({
5658
*/
5759
disabled ? true : undefined
5860
"
59-
:aria-keyshortcuts="ariaKeyshortcuts"
61+
:aria-keyshortcuts="keyboardShortcutsEnabled ? ariaKeyshortcuts : undefined"
6062
>
6163
<span v-if="classicon" class="size-[1em]" :class="classicon" aria-hidden="true" />
6264
<slot />
6365
<kbd
64-
v-if="ariaKeyshortcuts"
66+
v-if="keyboardShortcutsEnabled && ariaKeyshortcuts"
67+
data-kbd-hint
6568
class="ms-2 inline-flex items-center justify-center w-4 h-4 text-xs text-fg bg-bg-muted border border-border rounded no-underline"
6669
aria-hidden="true"
6770
>

app/components/Link/Base.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const isLink = computed(() => props.variant === 'link')
5959
const isButton = computed(() => !isLink.value)
6060
const isButtonSmall = computed(() => props.size === 'small' && !isLink.value)
6161
const isButtonMedium = computed(() => props.size === 'medium' && !isLink.value)
62+
const keyboardShortcutsEnabled = useKeyboardShortcuts()
6263
</script>
6364

6465
<template>
@@ -97,7 +98,7 @@ const isButtonMedium = computed(() => props.size === 'medium' && !isLink.value)
9798
variant === 'button-primary',
9899
}"
99100
:to="to"
100-
:aria-keyshortcuts="ariaKeyshortcuts"
101+
:aria-keyshortcuts="keyboardShortcutsEnabled ? ariaKeyshortcuts : undefined"
101102
:target="isLinkExternal ? '_blank' : undefined"
102103
>
103104
<span v-if="classicon" class="size-[1em]" :class="classicon" aria-hidden="true" />
@@ -114,7 +115,8 @@ const isButtonMedium = computed(() => props.size === 'medium' && !isLink.value)
114115
aria-hidden="true"
115116
/>
116117
<kbd
117-
v-if="ariaKeyshortcuts"
118+
v-if="keyboardShortcutsEnabled && ariaKeyshortcuts"
119+
data-kbd-hint
118120
class="ms-2 inline-flex items-center justify-center size-4 text-xs text-fg bg-bg-muted border border-border rounded no-underline"
119121
aria-hidden="true"
120122
>

app/composables/useSettings.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export interface AppSettings {
2929
selectedLocale: LocaleObject['code'] | null
3030
/** Search provider for package search */
3131
searchProvider: SearchProvider
32+
/** Enable/disable keyboard shortcuts */
33+
keyboardShortcuts: boolean
3234
/** Connector preferences */
3335
connector: {
3436
/** Automatically open the web auth page in the browser */
@@ -53,6 +55,7 @@ const DEFAULT_SETTINGS: AppSettings = {
5355
selectedLocale: null,
5456
preferredBackgroundTheme: null,
5557
searchProvider: import.meta.test ? 'npm' : 'algolia',
58+
keyboardShortcuts: true,
5659
connector: {
5760
autoOpenURL: false,
5861
},
@@ -97,6 +100,31 @@ export function useRelativeDates() {
97100
return computed(() => settings.value.relativeDates)
98101
}
99102

103+
/**
104+
* Composable for accessing just the keyboard shortcuts setting.
105+
* Useful for components that only need to read this specific setting.
106+
*/
107+
export const useKeyboardShortcuts = createSharedComposable(function useKeyboardShortcuts() {
108+
const { settings } = useSettings()
109+
const enabled = computed(() => settings.value.keyboardShortcuts)
110+
111+
if (import.meta.client) {
112+
watch(
113+
enabled,
114+
value => {
115+
if (value) {
116+
delete document.documentElement.dataset.kbdShortcuts
117+
} else {
118+
document.documentElement.dataset.kbdShortcuts = 'false'
119+
}
120+
},
121+
{ immediate: true },
122+
)
123+
}
124+
125+
return enabled
126+
})
127+
100128
/**
101129
* Composable for managing accent color.
102130
*/

app/pages/package/[[org]]/[name].vue

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -688,8 +688,10 @@ const codeLink = computed((): RouteLocationRaw | null => {
688688
}
689689
})
690690
691+
const keyboardShortcuts = useKeyboardShortcuts()
692+
691693
onKeyStroke(
692-
e => isKeyWithoutModifiers(e, '.') && !isEditableElement(e.target),
694+
e => keyboardShortcuts.value && isKeyWithoutModifiers(e, '.') && !isEditableElement(e.target),
693695
e => {
694696
if (codeLink.value === null) return
695697
e.preventDefault()
@@ -700,7 +702,7 @@ onKeyStroke(
700702
)
701703
702704
onKeyStroke(
703-
e => isKeyWithoutModifiers(e, 'd') && !isEditableElement(e.target),
705+
e => keyboardShortcuts.value && isKeyWithoutModifiers(e, 'd') && !isEditableElement(e.target),
704706
e => {
705707
if (!docsLink.value) return
706708
e.preventDefault()
@@ -710,7 +712,7 @@ onKeyStroke(
710712
)
711713
712714
onKeyStroke(
713-
e => isKeyWithoutModifiers(e, 'c') && !isEditableElement(e.target),
715+
e => keyboardShortcuts.value && isKeyWithoutModifiers(e, 'c') && !isEditableElement(e.target),
714716
e => {
715717
if (!pkg.value) return
716718
e.preventDefault()

app/pages/settings.vue

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ const { settings } = useSettings()
55
const { locale, locales, setLocale: setNuxti18nLocale } = useI18n()
66
const colorMode = useColorMode()
77
const { currentLocaleStatus, isSourceLocale } = useI18nStatus()
8+
const keyboardShortcutsEnabled = useKeyboardShortcuts()
89
910
// Escape to go back (but not when focused on form elements or modal is open)
1011
onKeyStroke(
1112
e =>
13+
keyboardShortcutsEnabled.value &&
1214
isKeyWithoutModifiers(e, 'Escape') &&
1315
!isEditableElement(e.target) &&
1416
!document.documentElement.matches('html:has(:modal)'),
@@ -205,6 +207,7 @@ const setLocale: typeof setNuxti18nLocale = locale => {
205207
</div>
206208
</section>
207209

210+
<!-- LANGUAGE Section -->
208211
<section>
209212
<h2 class="text-xs text-fg-muted uppercase tracking-wider mb-4">
210213
{{ $t('settings.sections.language') }}
@@ -260,6 +263,20 @@ const setLocale: typeof setNuxti18nLocale = locale => {
260263
</template>
261264
</div>
262265
</section>
266+
267+
<!-- KEYBOARD SHORTCUTS Section -->
268+
<section>
269+
<h2 class="text-xs text-fg-muted uppercase tracking-wider mb-4">
270+
{{ $t('settings.sections.keyboard_shortcuts') }}
271+
</h2>
272+
<div class="bg-bg-subtle border border-border rounded-lg p-4 sm:p-6">
273+
<SettingsToggle
274+
:label="$t('settings.keyboard_shortcuts_enabled')"
275+
:description="$t('settings.keyboard_shortcuts_enabled_description')"
276+
v-model="settings.keyboardShortcuts"
277+
/>
278+
</div>
279+
</section>
263280
</div>
264281
</article>
265282
</main>

app/utils/prehydrate.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,10 @@ export function initPreferencesOnPrehydrate() {
5353
document.documentElement.dataset.pm = pm
5454

5555
document.documentElement.dataset.collapsed = settings.sidebar?.collapsed?.join(' ') ?? ''
56+
57+
// Keyboard shortcuts (default: true)
58+
if (settings.keyboardShortcuts === false) {
59+
document.documentElement.dataset.kbdShortcuts = 'false'
60+
}
5661
})
5762
}

0 commit comments

Comments
 (0)