Skip to content

Commit 69d410c

Browse files
authored
Merge pull request #7401 from nextcloud-libraries/feat/NcKbd
feat: add NcKbd component
2 parents f40b24c + 0e596d4 commit 69d410c

File tree

6 files changed

+290
-1
lines changed

6 files changed

+290
-1
lines changed

l10n/messages.pot

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ msgstr ""
2727
msgid "Add to a project"
2828
msgstr ""
2929

30+
#. TRANSLATORS: Alt key on keyboard (Windows/Linux)
31+
msgid "Alt"
32+
msgstr ""
33+
3034
msgid "Animals & Nature"
3135
msgstr ""
3236

@@ -117,6 +121,10 @@ msgstr ""
117121
msgid "Connect items to a project to make them easier to find"
118122
msgstr ""
119123

124+
#. TRANSLATORS: Ctrl key on keyboard (Windows/Linux)
125+
msgid "Ctrl"
126+
msgstr ""
127+
120128
msgid "Custom"
121129
msgstr ""
122130

@@ -138,6 +146,10 @@ msgstr ""
138146
msgid "Decrement seconds"
139147
msgstr ""
140148

149+
#. TRANSLATORS: Delete key on keyboard
150+
msgid "Delete"
151+
msgstr ""
152+
141153
#. TRANSLATORS: A color name for RGB(136, 85, 168)
142154
msgid "Deluge"
143155
msgstr ""
@@ -157,12 +169,20 @@ msgstr ""
157169
msgid "Enable interactive view"
158170
msgstr ""
159171

172+
#. TRANSLATORS: Enter key on keyboard
173+
msgid "Enter"
174+
msgstr ""
175+
160176
msgid "Enter link"
161177
msgstr ""
162178

163179
msgid "Error getting related resources. Please contact your system administrator if you have any questions."
164180
msgstr ""
165181

182+
#. TRANSLATORS: Escape key on keyboard
183+
msgid "Escape"
184+
msgstr ""
185+
166186
msgid "External documentation"
167187
msgstr ""
168188

@@ -446,6 +466,10 @@ msgstr ""
446466
msgid "Settings navigation"
447467
msgstr ""
448468

469+
#. TRANSLATORS: Shift key on keyboard
470+
msgid "Shift"
471+
msgstr ""
472+
449473
msgid "Show details"
450474
msgstr ""
451475

@@ -467,6 +491,10 @@ msgstr ""
467491
msgid "Smileys & Emotion"
468492
msgstr ""
469493

494+
#. TRANSLATORS: Space key on keyboard
495+
msgid "Space"
496+
msgstr ""
497+
470498
msgid "Start slideshow"
471499
msgstr ""
472500

@@ -482,6 +510,10 @@ msgstr ""
482510
msgid "Symbols"
483511
msgstr ""
484512

513+
#. TRANSLATORS: Tab key on keyboard
514+
msgid "Tab"
515+
msgstr ""
516+
485517
msgid "Time picker"
486518
msgstr ""
487519

src/components/NcKbd/NcKbd.vue

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import type { Slot } from 'vue'
8+
9+
import { computed } from 'vue'
10+
import { t } from '../../l10n.ts'
11+
import { isMac } from '../../utils/platform.ts'
12+
13+
const {
14+
symbol = undefined,
15+
mac = isMac,
16+
} = defineProps<{
17+
/**
18+
* Key name or symbol to display.
19+
* For common special keys (CTRL, ALT, SHIFT, Arrow keys, etc.) it will display the symbol on macOS and the localized name on Windows/Linux.
20+
*/
21+
symbol?: 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight' | 'Control' | 'Alt' | 'Shift' | 'Enter' | 'Space' | 'Tab' | 'Delete' | 'Escape' | (string & {})
22+
/**
23+
* Explicitly use macOS (true) or Windows/Linux (false) key symbols.
24+
* By default it uses the OS detected from the user agent.
25+
*/
26+
mac?: boolean | undefined
27+
}>()
28+
29+
defineSlots<{
30+
default?: Slot
31+
}>()
32+
33+
/**
34+
* Map of special key names to symbols or localized names:
35+
* - macOS uses symbols instead of names
36+
* - Windows/Linux uses localized names
37+
* - In ternary expressions, // TRANSLATORS comments only works for the second operand, but not for `else`
38+
*/
39+
const labels = computed(() => ({
40+
ArrowUp: '',
41+
ArrowDown: '',
42+
ArrowLeft: '',
43+
ArrowRight: '',
44+
Control: !mac
45+
? t('Ctrl') // TRANSLATORS: Ctrl key on keyboard (Windows/Linux)
46+
: '',
47+
Alt: !mac
48+
? t('Alt') // TRANSLATORS: Alt key on keyboard (Windows/Linux)
49+
: '',
50+
Shift: !mac
51+
? t('Shift') // TRANSLATORS: Shift key on keyboard
52+
: '',
53+
Enter: !mac
54+
? t('Enter') // TRANSLATORS: Enter key on keyboard
55+
: '',
56+
Tab: !mac
57+
? t('Tab') // TRANSLATORS: Tab key on keyboard
58+
: '',
59+
Delete: !mac
60+
? t('Delete') // TRANSLATORS: Delete key on keyboard
61+
: '',
62+
Escape: !mac
63+
? t('Escape') // TRANSLATORS: Escape key on keyboard
64+
: '',
65+
Space: t('Space'), // TRANSLATORS: Space key on keyboard
66+
} as const))
67+
68+
const label = computed(() => (symbol && labels.value[symbol]) || symbol)
69+
</script>
70+
71+
<template>
72+
<kbd :class="$style.kbd">
73+
<slot>
74+
{{ label }}
75+
</slot>
76+
</kbd>
77+
</template>
78+
79+
<style lang="scss" module>
80+
.kbd {
81+
display: inline-flex;
82+
align-items: center;
83+
justify-content: center;
84+
min-width: var(--default-clickable-area);
85+
height: var(--default-clickable-area);
86+
padding-inline: calc(2 * var(--default-grid-baseline)) calc(2 * var(--default-grid-baseline));
87+
border: 2px solid var(--color-primary-element-light);
88+
border-block-end-width: 4px;
89+
border-radius: var(--border-radius-element);
90+
box-shadow: none; /* Override server <kbd> styles */
91+
font-family: var(--font-family); /* Design decision: looks better with the default font instead of mono */
92+
line-height: 1;
93+
white-space: nowrap;
94+
95+
& + .kbd {
96+
margin-inline-start: calc(1 * var(--default-grid-baseline));
97+
}
98+
}
99+
</style>
100+
101+
<docs>
102+
Nextcloud-styled `<kbd>` element. It can be used with a single key symbol in the content or with the `symbol` prop (preferred).
103+
104+
Subsequent `<NcKbd>` elements will have a small margin between them.
105+
106+
```vue
107+
<template>
108+
<div>
109+
<NcKbd symbol="Control" />
110+
<NcKbd symbol="F" />
111+
</div>
112+
</template>
113+
```
114+
115+
### Special symbols
116+
117+
It is recommended to use the `symbol` prop. It displays the appropriate symbol or localized name depending on the OS.
118+
OS detection is automatic but can be overridden via the `mac` prop.
119+
120+
```vue
121+
<template>
122+
<table class="sample-table">
123+
<tr>
124+
<th>symbol</th>
125+
<th>Auto</th>
126+
<th>macOS</th>
127+
<th>Windows/Linux</th>
128+
</tr>
129+
<tr>
130+
<th>ArrowUp</th>
131+
<td><NcKbd symbol="ArrowUp" /></td>
132+
<td><NcKbd symbol="ArrowUp" :mac="true" /></td>
133+
<td><NcKbd symbol="ArrowUp" :mac="false" /></td>
134+
</tr>
135+
<tr>
136+
<th>ArrowDown</th>
137+
<td><NcKbd symbol="ArrowDown" /></td>
138+
<td><NcKbd symbol="ArrowDown" :mac="true" /></td>
139+
<td><NcKbd symbol="ArrowDown" :mac="false" /></td>
140+
</tr>
141+
<tr>
142+
<th>ArrowLeft</th>
143+
<td><NcKbd symbol="ArrowLeft" /></td>
144+
<td><NcKbd symbol="ArrowLeft" :mac="true" /></td>
145+
<td><NcKbd symbol="ArrowLeft" :mac="false" /></td>
146+
</tr>
147+
<tr>
148+
<th>ArrowRight</th>
149+
<td><NcKbd symbol="ArrowRight" /></td>
150+
<td><NcKbd symbol="ArrowRight" :mac="true" /></td>
151+
<td><NcKbd symbol="ArrowRight" :mac="false" /></td>
152+
</tr>
153+
<tr>
154+
<th>Control</th>
155+
<td><NcKbd symbol="Control" /></td>
156+
<td><NcKbd symbol="Control" :mac="true" /></td>
157+
<td><NcKbd symbol="Control" :mac="false" /></td>
158+
</tr>
159+
<tr>
160+
<th>Alt</th>
161+
<td><NcKbd symbol="Alt" /></td>
162+
<td><NcKbd symbol="Alt" :mac="true" /></td>
163+
<td><NcKbd symbol="Alt" :mac="false" /></td>
164+
</tr>
165+
<tr>
166+
<th>Shift</th>
167+
<td><NcKbd symbol="Shift" /></td>
168+
<td><NcKbd symbol="Shift" :mac="true" /></td>
169+
<td><NcKbd symbol="Shift" :mac="false" /></td>
170+
</tr>
171+
<tr>
172+
<th>Enter</th>
173+
<td><NcKbd symbol="Enter" /></td>
174+
<td><NcKbd symbol="Enter" :mac="true" /></td>
175+
<td><NcKbd symbol="Enter" :mac="false" /></td>
176+
</tr>
177+
<tr>
178+
<th>Tab</th>
179+
<td><NcKbd symbol="Tab" /></td>
180+
<td><NcKbd symbol="Tab" :mac="true" /></td>
181+
<td><NcKbd symbol="Tab" :mac="false" /></td>
182+
</tr>
183+
<tr>
184+
<th>Delete</th>
185+
<td><NcKbd symbol="Delete" /></td>
186+
<td><NcKbd symbol="Delete" :mac="true" /></td>
187+
<td><NcKbd symbol="Delete" :mac="false" /></td>
188+
</tr>
189+
<tr>
190+
<th>Escape</th>
191+
<td><NcKbd symbol="Escape" /></td>
192+
<td><NcKbd symbol="Escape" :mac="true" /></td>
193+
<td><NcKbd symbol="Escape" :mac="false" /></td>
194+
</tr>
195+
<tr>
196+
<th>Space</th>
197+
<td><NcKbd symbol="Space" /></td>
198+
<td><NcKbd symbol="Space" :mac="true" /></td>
199+
<td><NcKbd symbol="Space" :mac="false" /></td>
200+
</tr>
201+
</table>
202+
</template>
203+
204+
<style scoped>
205+
.sample-table {
206+
border-collapse: collapse;
207+
208+
th,
209+
td {
210+
padding-inline: calc(2 * var(--default-grid-baseline));
211+
padding-block: calc(1 * var(--default-grid-baseline));
212+
}
213+
214+
tr:first-child {
215+
border-block-end: 2px solid var(--color-border);
216+
217+
th {
218+
font-weight: bold;
219+
}
220+
}
221+
}
222+
</style>
223+
```
224+
225+
### Custom content
226+
227+
In a special case you might want to use a custom content.
228+
229+
```vue
230+
<template>
231+
<NcKbd aria-label="Eject">
232+
<IconEject :size="15" />
233+
</NcKbd>
234+
</template>
235+
236+
<script>
237+
import IconEject from 'vue-material-design-icons/Eject.vue'
238+
239+
export default {
240+
components: { IconEject }
241+
}
242+
</script>
243+
```
244+
</docs>

src/components/NcKbd/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
export { default } from './NcKbd.vue'

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export { default as NcHeaderMenu } from './NcHeaderMenu/index.ts'
6060
export { default as NcHighlight } from './NcHighlight/index.ts'
6161
export { default as NcIconSvgWrapper } from './NcIconSvgWrapper/index.ts'
6262
export { default as NcInputField } from './NcInputField/index.ts'
63+
export { default as NcKbd } from './NcKbd/index.ts'
6364
export { default as NcListItem } from './NcListItem/index.js'
6465
export { default as NcListItemIcon } from './NcListItemIcon/index.js'
6566
export { default as NcLoadingIcon } from './NcLoadingIcon/index.ts'

src/composables/useHotKey/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55
import { onKeyStroke } from '@vueuse/core'
6+
import { isMac } from '../../utils/platform.ts'
67

78
const disableKeyboardShortcuts = window.OCP?.Accessibility?.disableKeyboardShortcuts?.()
8-
const isMac = /mac|ipad|iphone|darwin/i.test(navigator.userAgent)
99
const derivedKeysRegex = /^[a-zA-Z0-9]$/
1010
const nonAsciiPrintableRegex = /^[^\x20-\x7F]$/
1111

src/utils/platform.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
export const isMac = /mac|ipad|iphone|darwin/i.test(navigator.userAgent)

0 commit comments

Comments
 (0)