Skip to content

Commit bff7be6

Browse files
committed
feat: add keyboard shortcut toggle settings
1 parent 40d1964 commit bff7be6

12 files changed

Lines changed: 117 additions & 32 deletions

File tree

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/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_description" 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/Button/Base.vue

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script setup lang="ts">
2+
import { ClientOnly } from '#components'
23
import type { IconClass } from '~/types'
34
45
const props = withDefaults(
@@ -21,6 +22,8 @@ const props = withDefaults(
2122
2223
const el = useTemplateRef('el')
2324
25+
const keyboardShortcutsEnabled = computed(() => import.meta.client && useKeyboardShortcuts().value)
26+
2427
defineExpose({
2528
focus: () => el.value?.focus(),
2629
getBoundingClientRect: () => el.value?.getBoundingClientRect(),
@@ -51,16 +54,18 @@ defineExpose({
5154
*/
5255
disabled ? true : undefined
5356
"
54-
:aria-keyshortcuts="ariaKeyshortcuts"
57+
:aria-keyshortcuts="keyboardShortcutsEnabled ? ariaKeyshortcuts : undefined"
5558
>
5659
<span v-if="classicon" class="size-[1em]" :class="classicon" aria-hidden="true" />
5760
<slot />
58-
<kbd
59-
v-if="ariaKeyshortcuts"
60-
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"
61-
aria-hidden="true"
62-
>
63-
{{ ariaKeyshortcuts }}
64-
</kbd>
61+
<ClientOnly>
62+
<kbd
63+
v-if="keyboardShortcutsEnabled && ariaKeyshortcuts"
64+
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"
65+
aria-hidden="true"
66+
>
67+
{{ ariaKeyshortcuts }}
68+
</kbd>
69+
</ClientOnly>
6570
</button>
6671
</template>

app/components/Compare/PackageSelector.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ function removePackage(name: string) {
9898
}
9999
100100
function handleKeydown(e: KeyboardEvent) {
101+
if (!useKeyboardShortcuts().value) return
102+
101103
const items = navigableItems.value
102104
const count = items.length
103105

app/components/Link/Base.vue

Lines changed: 11 additions & 8 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 = computed(() => import.meta.client && useKeyboardShortcuts().value)
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" />
@@ -113,12 +114,14 @@ const isButtonMedium = computed(() => props.size === 'medium' && !isLink.value)
113114
class="i-lucide:link size-[1em] opacity-0 group-hover/link:opacity-100 transition-opacity duration-200"
114115
aria-hidden="true"
115116
/>
116-
<kbd
117-
v-if="ariaKeyshortcuts"
118-
class="ms-2 inline-flex items-center justify-center size-4 text-xs text-fg bg-bg-muted border border-border rounded no-underline"
119-
aria-hidden="true"
120-
>
121-
{{ ariaKeyshortcuts }}
122-
</kbd>
117+
<ClientOnly>
118+
<kbd
119+
v-if="keyboardShortcutsEnabled && ariaKeyshortcuts"
120+
class="ms-2 inline-flex items-center justify-center size-4 text-xs text-fg bg-bg-muted border border-border rounded no-underline"
121+
aria-hidden="true"
122+
>
123+
{{ ariaKeyshortcuts }}
124+
</kbd>
125+
</ClientOnly>
123126
</NuxtLink>
124127
</template>

app/composables/useSettings.ts

Lines changed: 12 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 */
@@ -48,6 +50,7 @@ const DEFAULT_SETTINGS: AppSettings = {
4850
selectedLocale: null,
4951
preferredBackgroundTheme: null,
5052
searchProvider: import.meta.test ? 'npm' : 'algolia',
53+
keyboardShortcuts: true,
5154
connector: {
5255
autoOpenURL: false,
5356
},
@@ -87,6 +90,15 @@ export function useRelativeDates() {
8790
return computed(() => settings.value.relativeDates)
8891
}
8992

93+
/**
94+
* Composable for accessing just the keyboard shortcuts setting.
95+
* Useful for components that only need to read this specific setting.
96+
*/
97+
export function useKeyboardShortcuts() {
98+
const { settings } = useSettings()
99+
return computed(() => settings.value.keyboardShortcuts)
100+
}
101+
90102
/**
91103
* Composable for managing accent color.
92104
*/

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -683,8 +683,10 @@ const codeLink = computed((): RouteLocationRaw | null => {
683683
}
684684
})
685685
686+
const keyboardShortcuts = useKeyboardShortcuts()
687+
686688
onKeyStroke(
687-
e => isKeyWithoutModifiers(e, '.') && !isEditableElement(e.target),
689+
e => keyboardShortcuts.value && isKeyWithoutModifiers(e, '.') && !isEditableElement(e.target),
688690
e => {
689691
if (codeLink.value === null) return
690692
e.preventDefault()
@@ -695,7 +697,7 @@ onKeyStroke(
695697
)
696698
697699
onKeyStroke(
698-
e => isKeyWithoutModifiers(e, 'd') && !isEditableElement(e.target),
700+
e => keyboardShortcuts.value && isKeyWithoutModifiers(e, 'd') && !isEditableElement(e.target),
699701
e => {
700702
if (!docsLink.value) return
701703
e.preventDefault()
@@ -705,7 +707,7 @@ onKeyStroke(
705707
)
706708
707709
onKeyStroke(
708-
e => isKeyWithoutModifiers(e, 'c') && !isEditableElement(e.target),
710+
e => keyboardShortcuts.value && isKeyWithoutModifiers(e, 'c') && !isEditableElement(e.target),
709711
e => {
710712
if (!pkg.value) return
711713
e.preventDefault()

app/pages/settings.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ const setLocale: typeof setNuxti18nLocale = locale => {
205205
</div>
206206
</section>
207207

208+
<!-- LANGUAGE Section -->
208209
<section>
209210
<h2 class="text-xs text-fg-muted uppercase tracking-wider mb-4">
210211
{{ $t('settings.sections.language') }}
@@ -260,6 +261,20 @@ const setLocale: typeof setNuxti18nLocale = locale => {
260261
</template>
261262
</div>
262263
</section>
264+
265+
<!-- KEYBOARD SHORTCUTS Section -->
266+
<section>
267+
<h2 class="text-xs text-fg-muted uppercase tracking-wider mb-4">
268+
{{ $t('settings.sections.keyboard_shortcuts') }}
269+
</h2>
270+
<div class="bg-bg-subtle border border-border rounded-lg p-4 sm:p-6">
271+
<SettingsToggle
272+
:label="$t('settings.keyboard_shortcuts_enabled')"
273+
:description="$t('settings.keyboard_shortcuts_enabled_description')"
274+
v-model="settings.keyboardShortcuts"
275+
/>
276+
</div>
277+
</section>
263278
</div>
264279
</article>
265280
</main>

i18n/locales/en.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
"navigate_results": "Navigate results",
3434
"go_to_result": "Go to result",
3535
"open_code_view": "Open code view",
36-
"open_docs": "Open docs"
36+
"open_docs": "Open docs",
37+
"disable_shortcuts": "Disable shortcuts",
38+
"disable_shortcuts_description": "You could disable keyboard shortcuts in {settings}."
3739
},
3840
"search": {
3941
"label": "Search npm packages",
@@ -84,7 +86,8 @@
8486
"appearance": "Appearance",
8587
"display": "Display",
8688
"search": "Data source",
87-
"language": "Language"
89+
"language": "Language",
90+
"keyboard_shortcuts": "Keyboard shortcuts"
8891
},
8992
"data_source": {
9093
"label": "Data source",
@@ -108,7 +111,9 @@
108111
"accent_colors": "Accent colors",
109112
"clear_accent": "Clear accent color",
110113
"translation_progress": "Translation progress",
111-
"background_themes": "Background shade"
114+
"background_themes": "Background shade",
115+
"keyboard_shortcuts_enabled": "Enable keyboard shortcuts",
116+
"keyboard_shortcuts_enabled_description": "Keyboard shortcuts can be disabled if they conflict with other browser or system shortcuts"
112117
},
113118
"i18n": {
114119
"missing_keys": "{count} missing translation | {count} missing translations",

i18n/schema.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@
105105
},
106106
"open_docs": {
107107
"type": "string"
108+
},
109+
"disable_shortcuts": {
110+
"type": "string"
111+
},
112+
"disable_shortcuts_description": {
113+
"type": "string"
108114
}
109115
},
110116
"additionalProperties": false
@@ -258,6 +264,9 @@
258264
},
259265
"language": {
260266
"type": "string"
267+
},
268+
"keyboard_shortcuts": {
269+
"type": "string"
261270
}
262271
},
263272
"additionalProperties": false
@@ -330,6 +339,12 @@
330339
},
331340
"background_themes": {
332341
"type": "string"
342+
},
343+
"keyboard_shortcuts_enabled": {
344+
"type": "string"
345+
},
346+
"keyboard_shortcuts_enabled_description": {
347+
"type": "string"
333348
}
334349
},
335350
"additionalProperties": false

0 commit comments

Comments
 (0)