feat(quick-261022-k9k): admin save shortcuts and Create and new on record forms
- app/hotkeys.ts: dependency-free data-hotkey core (Ctrl/Cmd+S, Ctrl/Cmd+Enter, Ctrl/Cmd+Shift+S, Esc, / and ?), innermost-modal root, capture-phase modifier listener, platform labels and the help-dialog state - Button hotkey prop (data-hotkey, aria-keyshortcuts); shortcuts installed by AppShell - FormView: save keys and tooltips, Create and new on create forms via remountView - phrasebook: form.create_and_new and the shortcuts group (en, pl)
This commit is contained in:
21
admin/tests/fixtures/lang.json
vendored
21
admin/tests/fixtures/lang.json
vendored
@@ -246,6 +246,9 @@
|
||||
"backend::lang.form.create": {
|
||||
"other": "Utwórz"
|
||||
},
|
||||
"backend::lang.form.create_and_new": {
|
||||
"other": "Utwórz i nowy"
|
||||
},
|
||||
"backend::lang.form.delete": {
|
||||
"other": "Usuń"
|
||||
},
|
||||
@@ -845,6 +848,24 @@
|
||||
},
|
||||
"backend::lang.settings.title": {
|
||||
"other": "Ustawienia"
|
||||
},
|
||||
"backend::lang.shortcuts.description": {
|
||||
"other": "Skróty dostępne na tym ekranie."
|
||||
},
|
||||
"backend::lang.shortcuts.help": {
|
||||
"other": "Pokaż skróty klawiaturowe"
|
||||
},
|
||||
"backend::lang.shortcuts.hint": {
|
||||
"other": ":label (:keys)"
|
||||
},
|
||||
"backend::lang.shortcuts.leave": {
|
||||
"other": "Anuluj i wróć"
|
||||
},
|
||||
"backend::lang.shortcuts.search": {
|
||||
"other": "Przejdź do wyszukiwania"
|
||||
},
|
||||
"backend::lang.shortcuts.title": {
|
||||
"other": "Skróty klawiaturowe"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
|
||||
68
admin/tests/smoke/shortcuts.smoke.test.ts
Normal file
68
admin/tests/smoke/shortcuts.smoke.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
// Keyboard shortcuts on the whole app (mounted with attachTo, since the
|
||||
// shortcut listener reads the document): the save keys on record forms.
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { enableAutoUnmount, flushPromises } from '@vue/test-utils'
|
||||
import { setBundle } from '../../src/app/i18n'
|
||||
import { clearUser } from '../../src/state/useAuth'
|
||||
import { setNavigation } from '../../src/state/useNavigation'
|
||||
import { clearToasts } from '../../src/state/useToasts'
|
||||
import lang from '../fixtures/lang.json'
|
||||
import formSchema from '../fixtures/widgets.form-schema.json'
|
||||
import record from '../fixtures/widgets.record.json'
|
||||
import { API, mountApp, requestsTo, type Route } from '../helpers'
|
||||
|
||||
const RECORD = `${API}/acme/demo/widgets/1`
|
||||
|
||||
const formRoutes: Record<string, Route> = {
|
||||
[`GET ${API}/acme/demo/widgets/schema/form`]: { body: formSchema },
|
||||
[`GET ${RECORD}`]: { body: record },
|
||||
[`PUT ${RECORD}`]: { body: record },
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
clearUser()
|
||||
setNavigation([])
|
||||
clearToasts()
|
||||
setBundle(lang.data, 'pl')
|
||||
vi.spyOn(navigator, 'platform', 'get').mockReturnValue('Linux x86_64')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
// Registered after the cleanup above, so it runs first (hooks run as a
|
||||
// stack): the app unmounts, with its listeners and portals, before the body
|
||||
// is cleared.
|
||||
enableAutoUnmount(afterEach)
|
||||
|
||||
function press(target: Element, key: string, init: KeyboardEventInit = {}): KeyboardEvent {
|
||||
const event = new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true, ...init })
|
||||
target.dispatchEvent(event)
|
||||
return event
|
||||
}
|
||||
|
||||
describe('record form save shortcuts', () => {
|
||||
it('saves an update form on Ctrl+S and Cmd+S without the browser Save page', async () => {
|
||||
const { calls } = await mountApp('/acme/demo/widgets/1', formRoutes, { attach: true })
|
||||
|
||||
const ctrl = press(document.body, 's', { ctrlKey: true })
|
||||
expect(ctrl.defaultPrevented).toBe(true)
|
||||
await flushPromises()
|
||||
expect(requestsTo(calls, 'PUT', RECORD)).toHaveLength(1)
|
||||
|
||||
const cmd = press(document.body, 's', { metaKey: true })
|
||||
expect(cmd.defaultPrevented).toBe(true)
|
||||
await flushPromises()
|
||||
expect(requestsTo(calls, 'PUT', RECORD)).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('shows the shortcut in the save button tooltip', async () => {
|
||||
const { wrapper } = await mountApp('/acme/demo/widgets/1', formRoutes, { attach: true })
|
||||
|
||||
const save = wrapper.get('[data-action="save"]')
|
||||
expect(save.attributes('title')).toBe('Zapisz (Ctrl+S)')
|
||||
expect(save.attributes('aria-keyshortcuts')).toBe('Control+S Meta+S')
|
||||
expect(wrapper.get('[data-action="save-close"]').attributes('title')).toBe('Zapisz i zamknij (Ctrl+Enter)')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user