diff --git a/packages/design-system/src/components/OcModal/OcModal.spec.ts b/packages/design-system/src/components/OcModal/OcModal.spec.ts index 5d538cb998..a779ed1620 100644 --- a/packages/design-system/src/components/OcModal/OcModal.spec.ts +++ b/packages/design-system/src/components/OcModal/OcModal.spec.ts @@ -49,6 +49,40 @@ describe('OcModal', () => { expect(wrapper.html()).toMatchSnapshot() }) + describe('enter key on the input', () => { + const mountInputModal = () => + mount(Modal, { + global: { + renderStubDefaultSlot: true, + plugins: [...defaultPlugins()], + stubs: { 'focus-trap': true } + }, + props: inputProps + }) + + it('emits confirm', async () => { + const wrapper = mountInputModal() + await wrapper.find('.oc-modal-body-input input').trigger('keydown', { key: 'Enter' }) + expect(wrapper.emitted('confirm')).toEqual([['New folder']]) + }) + + it('does not emit confirm while an IME composition session is active', async () => { + const wrapper = mountInputModal() + await wrapper + .find('.oc-modal-body-input input') + .trigger('keydown', { key: 'Enter', isComposing: true }) + expect(wrapper.emitted('confirm')).toBeUndefined() + }) + + it('does not emit confirm when the keydown event has keyCode 229', async () => { + const wrapper = mountInputModal() + await wrapper + .find('.oc-modal-body-input input') + .trigger('keydown', { key: 'Enter', keyCode: 229 }) + expect(wrapper.emitted('confirm')).toBeUndefined() + }) + }) + it('displays loading state', async () => { const waitForSpinnerToShow = async () => { await wrapper.vm.$nextTick() diff --git a/packages/design-system/src/components/OcModal/OcModal.vue b/packages/design-system/src/components/OcModal/OcModal.vue index 63cc8b2539..8726b67175 100644 --- a/packages/design-system/src/components/OcModal/OcModal.vue +++ b/packages/design-system/src/components/OcModal/OcModal.vue @@ -70,7 +70,7 @@ :fix-message-line="true" :selection-range="inputSelectionRange" @update:model-value="inputOnInput" - @keydown.enter.prevent="confirm" + @keydown.enter.prevent="onInputKeydownEnter" /> @@ -336,6 +336,14 @@ const confirm = () => { emit('confirm', unref(userInputValue)) } +const onInputKeydownEnter = (event: KeyboardEvent) => { + // ignore the enter confirming an IME composition (keyCode 229 for Safari) + if (event.isComposing || event.keyCode === 229) { + return + } + confirm() +} + const inputOnInput = (value: string) => { emit('input', value) } diff --git a/packages/web-app-external/src/components/FileNameModal.vue b/packages/web-app-external/src/components/FileNameModal.vue index d5f3b28e73..fb4a3d1220 100644 --- a/packages/web-app-external/src/components/FileNameModal.vue +++ b/packages/web-app-external/src/components/FileNameModal.vue @@ -9,7 +9,7 @@ :error-message="errorMessage" :fix-message-line="true" :selection-range="inputSelectionRange" - @keydown.enter.prevent="emit('confirm')" + @keydown.enter.prevent="onKeydownEnter" /> @@ -23,6 +23,7 @@ import { SpaceResource } from '@opencloud-eu/web-client' import { + isComposingEvent, Modal, resolveFileNameDuplicate, useClientService, @@ -90,6 +91,13 @@ const errorMessage = computed(() => { return undefined }) +const onKeydownEnter = (event: KeyboardEvent) => { + if (isComposingEvent(event)) { + return + } + emit('confirm') +} + const onConfirm = async () => { await callbackFn(unref(newFileName)) } diff --git a/packages/web-app-files/src/components/Modals/SetLinkPasswordModal.vue b/packages/web-app-files/src/components/Modals/SetLinkPasswordModal.vue index ee26d3b76d..0d9a527d8b 100644 --- a/packages/web-app-files/src/components/Modals/SetLinkPasswordModal.vue +++ b/packages/web-app-files/src/components/Modals/SetLinkPasswordModal.vue @@ -11,7 +11,7 @@ required-mark @password-challenge-completed="$emit('update:confirmDisabled', false)" @password-challenge-failed="$emit('update:confirmDisabled', true)" - @keydown.enter.prevent="$emit('confirm')" + @keydown.enter.prevent="onKeydownEnter" @update:model-value="onInput" /> @@ -21,6 +21,7 @@ import { defineComponent, ref, unref, PropType } from 'vue' import { useGettext } from 'vue3-gettext' import { upperFirst } from 'lodash-es' import { + isComposingEvent, Modal, useClientService, useMessages, @@ -54,6 +55,13 @@ export default defineComponent({ emit('update:confirmDisabled', true) + const onKeydownEnter = (event: KeyboardEvent) => { + if (isComposingEvent(event)) { + return + } + emit('confirm') + } + const onInput = (value: string) => { password.value = value errorMessage.value = undefined @@ -97,6 +105,7 @@ export default defineComponent({ return { password, onInput, + onKeydownEnter, errorMessage, passwordPolicyService, inputPasswordPolicy: passwordPolicyService.getPolicy({ enforcePassword: true }), diff --git a/packages/web-app-files/tests/unit/components/Modals/SetLinkPasswordModal.spec.ts b/packages/web-app-files/tests/unit/components/Modals/SetLinkPasswordModal.spec.ts index 0c534bcfdd..0af6b00157 100644 --- a/packages/web-app-files/tests/unit/components/Modals/SetLinkPasswordModal.spec.ts +++ b/packages/web-app-files/tests/unit/components/Modals/SetLinkPasswordModal.spec.ts @@ -25,6 +25,20 @@ describe('SetLinkPasswordModal', () => { const emitted = wrapper.emitted('update:confirmDisabled')! expect(emitted[emitted.length - 1]).toEqual([true]) }) + describe('method "onKeydownEnter"', () => { + it('emits confirm', () => { + const { wrapper } = getWrapper() + wrapper.vm.onKeydownEnter(new KeyboardEvent('keydown', { key: 'Enter' })) + + expect(wrapper.emitted('confirm')).toBeTruthy() + }) + it('does not emit confirm while an IME composition session is active', () => { + const { wrapper } = getWrapper() + wrapper.vm.onKeydownEnter(new KeyboardEvent('keydown', { key: 'Enter', isComposing: true })) + + expect(wrapper.emitted('confirm')).toBeUndefined() + }) + }) describe('method "onConfirm"', () => { it('updates the link', async () => { const { wrapper } = getWrapper() diff --git a/packages/web-pkg/src/components/CreateShortcutModal.vue b/packages/web-pkg/src/components/CreateShortcutModal.vue index 1f1e64b677..e45e628d50 100644 --- a/packages/web-pkg/src/components/CreateShortcutModal.vue +++ b/packages/web-pkg/src/components/CreateShortcutModal.vue @@ -143,7 +143,7 @@ import { useGettext } from 'vue3-gettext' import DOMPurify from 'dompurify' import Mark from 'mark.js' import { OcDrop } from '@opencloud-eu/design-system/components' -import { resolveFileNameDuplicate } from '../helpers' +import { isComposingEvent, resolveFileNameDuplicate } from '../helpers' import { useTask } from 'vue-concurrency' import { debounce } from 'lodash-es' import ResourcePreview from './Search/ResourcePreview.vue' @@ -320,8 +320,8 @@ export default defineComponent({ unref(dropRef).hide() } - const onKeyEnterDrop = (e: Event) => { - if (!unref(isDropOpen)) { + const onKeyEnterDrop = (e: KeyboardEvent) => { + if (isComposingEvent(e) || !unref(isDropOpen)) { return } diff --git a/packages/web-pkg/src/helpers/index.ts b/packages/web-pkg/src/helpers/index.ts index feb3ef79ed..d02068c1ec 100644 --- a/packages/web-pkg/src/helpers/index.ts +++ b/packages/web-pkg/src/helpers/index.ts @@ -13,6 +13,7 @@ export * from './folderVault' export * from './vaultEngine' export * from './filesize' export * from './fuse' +export * from './keyboard' export * from './locale' export * from './path' export * from './permissions' diff --git a/packages/web-pkg/src/helpers/keyboard.ts b/packages/web-pkg/src/helpers/keyboard.ts new file mode 100644 index 0000000000..e38434fca2 --- /dev/null +++ b/packages/web-pkg/src/helpers/keyboard.ts @@ -0,0 +1,5 @@ +// Whether the event is part of an IME composition session, e.g. the Enter confirming a CJK +// conversion. keyCode 229 covers browsers where `isComposing` is already unset (e.g. Safari). +export const isComposingEvent = (event: KeyboardEvent) => { + return event.isComposing || event.keyCode === 229 +} diff --git a/packages/web-pkg/tests/unit/components/CreateShortcutModal.spec.ts b/packages/web-pkg/tests/unit/components/CreateShortcutModal.spec.ts index 8fc51a32b6..2ccac3b644 100644 --- a/packages/web-pkg/tests/unit/components/CreateShortcutModal.spec.ts +++ b/packages/web-pkg/tests/unit/components/CreateShortcutModal.spec.ts @@ -34,6 +34,28 @@ describe('CreateShortcutModal', () => { expect(showErrorMessage).toHaveBeenCalled() }) }) + describe('method "onKeyEnterDrop"', () => { + it('handles the event when the drop is open', () => { + const { wrapper } = getWrapper() + wrapper.vm.onShowDrop() + // move the active drop item index out of range to not interact with the stubbed drop + wrapper.vm.onKeyUpDrop() + const event = new KeyboardEvent('keydown', { key: 'Enter' }) + const stopPropagation = vi.spyOn(event, 'stopPropagation') + wrapper.vm.onKeyEnterDrop(event) + + expect(stopPropagation).toHaveBeenCalled() + }) + it('ignores the event while an IME composition session is active', () => { + const { wrapper } = getWrapper() + wrapper.vm.onShowDrop() + const event = new KeyboardEvent('keydown', { key: 'Enter', isComposing: true }) + const stopPropagation = vi.spyOn(event, 'stopPropagation') + wrapper.vm.onKeyEnterDrop(event) + + expect(stopPropagation).not.toHaveBeenCalled() + }) + }) describe('method "searchTask"', () => { it('should set "searchResult" correctly', async () => { const { wrapper } = getWrapper() diff --git a/packages/web-pkg/tests/unit/helpers/keyboard.spec.ts b/packages/web-pkg/tests/unit/helpers/keyboard.spec.ts new file mode 100644 index 0000000000..6e6941039e --- /dev/null +++ b/packages/web-pkg/tests/unit/helpers/keyboard.spec.ts @@ -0,0 +1,18 @@ +import { isComposingEvent } from '../../../src/helpers' + +describe('isComposingEvent', () => { + it('returns true when the event is part of an IME composition session', () => { + const event = new KeyboardEvent('keydown', { key: 'Enter', isComposing: true }) + expect(isComposingEvent(event)).toBe(true) + }) + + it('returns true when the event has keyCode 229', () => { + const event = new KeyboardEvent('keydown', { key: 'Enter', keyCode: 229 } as KeyboardEventInit) + expect(isComposingEvent(event)).toBe(true) + }) + + it('returns false for a regular keyboard event', () => { + const event = new KeyboardEvent('keydown', { key: 'Enter' }) + expect(isComposingEvent(event)).toBe(false) + }) +})