Skip to content

Commit 7e35a17

Browse files
authored
feat(keyboard-shortcut): add component Al 2134 (#271)
* feat(keyboard-shortcut): add component * chore: set up npm token for private packages * chore: move toolbox to devDeps, add missing external deps in vite config
1 parent 7a3f5d1 commit 7e35a17

File tree

10 files changed

+374
-2
lines changed

10 files changed

+374
-2
lines changed

.github/workflows/chromatic.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ jobs:
1111
uses: actions/checkout@v3
1212
with:
1313
fetch-depth: 0 # Required by Chromatic to retrieve git history
14+
- name: Setup npm auth token
15+
run: npm config set //registry.npmjs.org/:_authToken=${{secrets.CI_NPM_TOKEN_READONLY}}
1416
- name: Set up node
1517
uses: actions/setup-node@v3
1618
with:

package-lock.json

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"tabbable": "^6.2.0"
6868
},
6969
"devDependencies": {
70+
"@archilogic/toolbox": "^3.11.0",
7071
"@archilogic/eslint-config": "^2.0.0",
7172
"@archilogic/prettier": "^1.0.1",
7273
"@semantic-release/changelog": "^6.0.3",
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<script lang="ts">
2+
import { defineComponent, PropType, computed } from 'vue'
3+
import { isMac } from '@archilogic/toolbox'
4+
5+
type ShortcutModifier =
6+
| 'alt' // Alt on Windows, Option ⌥ on Mac
7+
| 'ctrl'
8+
| 'meta' // Windows Key on Windows, Command ⌘ on Mac
9+
| 'shift'
10+
11+
type ShortcutMouseEvent = 'click' | 'drag' | 'double-click'
12+
13+
export type Shortcut =
14+
| { modifiers: ShortcutModifier[] }
15+
| { keySequence: string }
16+
| { modifiers: ShortcutModifier[]; keySequence: string }
17+
| {
18+
modifiers: ShortcutModifier[]
19+
mouseEvent: ShortcutMouseEvent
20+
}
21+
| {
22+
mouseEvent: ShortcutMouseEvent
23+
}
24+
25+
const getShortcutKeys = (shortcut: Shortcut) => {
26+
const keys = []
27+
if ('modifiers' in shortcut) {
28+
if (shortcut.modifiers.includes('alt')) {
29+
keys.push(isMac ? '' : 'Alt')
30+
}
31+
if (shortcut.modifiers.includes('meta')) {
32+
keys.push(isMac ? '' : '')
33+
}
34+
if (shortcut.modifiers.includes('ctrl')) {
35+
keys.push('Ctrl')
36+
}
37+
if (shortcut.modifiers.includes('shift')) {
38+
keys.push('')
39+
}
40+
}
41+
if ('keySequence' in shortcut) {
42+
switch (shortcut.keySequence) {
43+
case 'delete':
44+
keys.push('Delete')
45+
break
46+
case 'backspace':
47+
keys.push('')
48+
break
49+
case ' ':
50+
keys.push('Space')
51+
break
52+
case 'escape':
53+
keys.push('Esc')
54+
break
55+
case 'enter':
56+
keys.push('')
57+
break
58+
case 'alt':
59+
keys.push(isMac ? '' : 'Alt')
60+
break
61+
default:
62+
keys.push(shortcut.keySequence.toUpperCase())
63+
break
64+
}
65+
}
66+
if ('mouseEvent' in shortcut) {
67+
keys.push(shortcut.mouseEvent)
68+
}
69+
return keys
70+
}
71+
72+
export default defineComponent({
73+
name: 'AKeyboardShortcut',
74+
props: {
75+
shortcut: {
76+
type: [Object, Array] as PropType<Shortcut | Shortcut[]>,
77+
required: true
78+
},
79+
variant: {
80+
type: String as PropType<'default' | 'subtle'>,
81+
default: 'default'
82+
}
83+
},
84+
setup(props) {
85+
const shortcuts = computed(() => {
86+
if (Array.isArray(props.shortcut)) {
87+
return props.shortcut.map(shortcut => ({
88+
keys: getShortcutKeys(shortcut)
89+
}))
90+
} else {
91+
return [{ keys: getShortcutKeys(props.shortcut) }]
92+
}
93+
})
94+
95+
return {
96+
shortcuts
97+
}
98+
}
99+
})
100+
</script>
101+
<template>
102+
<div class="flex items-center gap-2">
103+
<template v-for="(item, index) in shortcuts" :key="'shortcut' + index">
104+
<div :class="{ 'flex gap-[2px]': item.keys.length > 1 }">
105+
<template v-for="key in item.keys" :key="key">
106+
<kbd
107+
class="text-stone body-xs-600"
108+
:class="{ 'px-1 py-[2px] bg-athens rounded-md': variant === 'default' }">
109+
{{ key }}
110+
</kbd>
111+
</template>
112+
</div>
113+
<div v-if="index < shortcuts.length - 1" :key="'separator' + index" class="body-xs-400">
114+
or
115+
</div>
116+
</template>
117+
</div>
118+
</template>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { render } from '@testing-library/vue'
2+
import KeyboardShortcut from '../KeyboardShortcut.vue'
3+
4+
vi.mock('@archilogic/toolbox', async importOriginal => {
5+
const toolbox = await importOriginal<typeof import('@archilogic/toolbox')>()
6+
return {
7+
...toolbox,
8+
isMac: true
9+
}
10+
})
11+
12+
describe('when on Mac', () => {
13+
it('renders a Mac "alt" modifier key', async () => {
14+
const KeyboardShortcutMocked = await import('../KeyboardShortcut.vue')
15+
const { getByText } = render(KeyboardShortcutMocked.default, {
16+
props: { shortcut: { modifiers: ['alt'] } }
17+
})
18+
expect(getByText('⌥')).toBeTruthy()
19+
})
20+
21+
it('renders a Mac "meta" modifier key', () => {
22+
const { getByText } = render(KeyboardShortcut, {
23+
props: { shortcut: { modifiers: ['meta'] } }
24+
})
25+
expect(getByText('⌘')).toBeTruthy()
26+
})
27+
})
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { render } from '@testing-library/vue'
2+
import KeyboardShortcut from '../KeyboardShortcut.vue'
3+
4+
vi.mock('@archilogic/toolbox', async importOriginal => {
5+
const toolbox = await importOriginal<typeof import('@archilogic/toolbox')>()
6+
return {
7+
...toolbox,
8+
isMac: false
9+
}
10+
})
11+
describe('KeyboardShortcut', () => {
12+
describe('when given a single shortcut', () => {
13+
it('renders the shortcut', () => {
14+
const { getByText } = render(KeyboardShortcut, {
15+
props: { shortcut: { keySequence: 'a' } }
16+
})
17+
expect(getByText('A')).toBeTruthy()
18+
})
19+
})
20+
21+
describe('when given several shortcuts', () => {
22+
it('renders the shortcuts, separated by "or"s', () => {
23+
const { getByText, queryAllByText } = render(KeyboardShortcut, {
24+
props: { shortcut: [{ keySequence: 'a' }, { keySequence: 'b' }, { keySequence: 'c' }] }
25+
})
26+
expect(getByText('A')).toBeTruthy()
27+
expect(getByText('B')).toBeTruthy()
28+
expect(getByText('C')).toBeTruthy()
29+
expect(queryAllByText('or').length).toEqual(2)
30+
})
31+
})
32+
33+
it('renders a "ctrl" modifier key', () => {
34+
const { getByText } = render(KeyboardShortcut, {
35+
props: { shortcut: { modifiers: ['ctrl'] } }
36+
})
37+
38+
expect(getByText('Ctrl')).toBeTruthy()
39+
})
40+
41+
it('renders a "shift" modifier key', () => {
42+
const { getByText } = render(KeyboardShortcut, {
43+
props: { shortcut: { modifiers: ['shift'] } }
44+
})
45+
expect(getByText('⇧')).toBeTruthy()
46+
})
47+
48+
it('renders a "backspace" key', () => {
49+
const { getByText } = render(KeyboardShortcut, {
50+
props: { shortcut: { keySequence: 'backspace' } }
51+
})
52+
expect(getByText('⌫')).toBeTruthy()
53+
})
54+
55+
it('renders a "delete" key', () => {
56+
const { getByText } = render(KeyboardShortcut, {
57+
props: { shortcut: { keySequence: 'delete' } }
58+
})
59+
expect(getByText('Delete')).toBeTruthy()
60+
})
61+
62+
it('renders a "space" key', () => {
63+
const { getByText } = render(KeyboardShortcut, {
64+
props: { shortcut: { keySequence: ' ' } }
65+
})
66+
expect(getByText('Space')).toBeTruthy()
67+
})
68+
69+
it('renders an "escape" key', () => {
70+
const { getByText } = render(KeyboardShortcut, {
71+
props: { shortcut: { keySequence: 'escape' } }
72+
})
73+
expect(getByText('Esc')).toBeTruthy()
74+
})
75+
76+
it('renders an "enter" key', () => {
77+
const { getByText } = render(KeyboardShortcut, {
78+
props: { shortcut: { keySequence: 'enter' } }
79+
})
80+
expect(getByText('↵')).toBeTruthy()
81+
})
82+
83+
it('renders a key or key sequence in upper case', () => {
84+
const { getByText } = render(KeyboardShortcut, {
85+
props: { shortcut: { keySequence: 'wa' } }
86+
})
87+
expect(getByText('WA')).toBeTruthy()
88+
})
89+
90+
it('renders a mouse event', () => {
91+
const { getByText } = render(KeyboardShortcut, {
92+
props: { shortcut: { mouseEvent: 'click' } }
93+
})
94+
expect(getByText('click')).toBeTruthy()
95+
})
96+
97+
describe('when not on Mac', () => {
98+
it('renders an "alt" modifier key', () => {
99+
const { getByText } = render(KeyboardShortcut, {
100+
props: { shortcut: { modifiers: ['alt'] } }
101+
})
102+
expect(getByText('Alt')).toBeTruthy()
103+
})
104+
105+
it('renders a "meta" modifier key', () => {
106+
const { getByText } = render(KeyboardShortcut, {
107+
props: { shortcut: { modifiers: ['meta'] } }
108+
})
109+
expect(getByText('⊞')).toBeTruthy()
110+
})
111+
})
112+
})

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,4 @@ export { default as APanelCombobox } from './PanelCombobox.vue'
5050
export { default as APopupCombobox } from './PopupCombobox.vue'
5151
export { default as ASwitcher } from './Switcher.vue'
5252
export { default as ATooltip } from './Tooltip.vue'
53+
export { default as AKeyboardShortcut } from './KeyboardShortcut.vue'

0 commit comments

Comments
 (0)