Skip to content

Commit a780d4a

Browse files
authored
Merge pull request #4294 from nextcloud/fix/richcontenteditable-IME-support
NcRichContenteditable: fix IME support on Mac OSX
2 parents 840c53d + 990532c commit a780d4a

File tree

3 files changed

+143
-5
lines changed

3 files changed

+143
-5
lines changed

jest.config.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
*/
2222

2323
const ignorePatterns = [
24+
'ansi-regex',
2425
'bail',
26+
'char-regex',
2527
'comma-separated-tokens',
2628
'decode-named-character-reference',
2729
'escape-string-regexp',
@@ -33,6 +35,9 @@ const ignorePatterns = [
3335
'rehype-*',
3436
'remark-*',
3537
'space-separated-tokens',
38+
'string-length',
39+
'strip-ansi',
40+
'tributejs',
3641
'trim-lines',
3742
'trough',
3843
'unified',
@@ -62,6 +67,10 @@ module.exports = {
6267
'/node_modules/(?!(' + ignorePatterns.join('|') + '))',
6368
],
6469

70+
moduleNameMapper: {
71+
'\\.(css|scss)$': 'jest-transform-stub',
72+
},
73+
6574
snapshotSerializers: [
6675
'<rootDir>/node_modules/jest-serializer-vue',
6776
],

src/components/NcRichContenteditable/NcRichContenteditable.vue

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ export default {
162162
role="textbox"
163163
v-on="listeners"
164164
@input="onInput"
165+
@compositionstart="isComposing = true"
166+
@compositionend="isComposing = false"
165167
@keydown.delete="onDelete"
166168
@keydown.enter.exact="onEnter"
167169
@keydown.ctrl.enter.exact.stop.prevent="onCtrlEnter"
@@ -364,6 +366,9 @@ export default {
364366
// serves no other purpose than to check whether the
365367
// content is empty or not
366368
localValue: this.value,
369+
370+
// Is in text composition session in IME
371+
isComposing: false,
367372
}
368373
},
369374
@@ -672,17 +677,20 @@ export default {
672677
event.preventDefault()
673678
}
674679
},
675-
676680
/**
677681
* Enter key pressed. Submits if not multiline
678682
*
679683
* @param {Event} event the keydown event
680684
*/
681685
onEnter(event) {
682-
// Prevent submitting if autocompletion menu
683-
// is opened or length is over maxlength
684-
if (this.multiline || this.isOverMaxlength
685-
|| this.autocompleteTribute.isActive || this.emojiTribute.isActive || this.linkTribute.isActive) {
686+
// Prevent submitting if multiline
687+
// or length is over maxlength
688+
// or autocompletion menu is opened
689+
// or in a text composition session with IME
690+
if (this.multiline
691+
|| this.isOverMaxlength
692+
|| this.autocompleteTribute.isActive || this.emojiTribute.isActive || this.linkTribute.isActive
693+
|| this.isComposing) {
686694
return
687695
}
688696
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { mount } from '@vue/test-utils'
2+
import NcRichContenteditable from '../../../../src/components/NcRichContenteditable/NcRichContenteditable.vue'
3+
import Tribute from 'tributejs/dist/tribute.esm.js'
4+
5+
// FIXME: find a way to use Tribute in JSDOM or test with e2e
6+
jest.mock('tributejs/dist/tribute.esm.js')
7+
Tribute.mockImplementation(() => ({
8+
attach: jest.fn(),
9+
detach: jest.fn(),
10+
}))
11+
12+
/**
13+
* Mount NcRichContentEditable
14+
*
15+
* @param {object} options mount options
16+
* @param {object} options.propsData mount options.propsData
17+
* @param {object} options.listeners mount options.listeners
18+
* @param {object} options.attrs mount options.attrs
19+
* @return {object}
20+
*/
21+
function mountNcRichContenteditable({ propsData, listeners, attrs } = {}) {
22+
let currentValue = propsData?.value
23+
24+
const wrapper = mount(NcRichContenteditable, {
25+
propsData: {
26+
value: currentValue,
27+
...propsData,
28+
},
29+
listeners: {
30+
'update:value': ($event) => {
31+
currentValue = $event
32+
wrapper.setProps({ value: $event })
33+
},
34+
...listeners,
35+
},
36+
attrs: {
37+
...attrs,
38+
},
39+
attachTo: document.body,
40+
})
41+
42+
const getCurrentValue = () => currentValue
43+
44+
const inputValue = async (newValue) => {
45+
wrapper.element.innerHTML += newValue
46+
await wrapper.trigger('input')
47+
}
48+
49+
return {
50+
wrapper,
51+
getCurrentValue,
52+
inputValue,
53+
}
54+
}
55+
56+
describe('NcRichContenteditable', () => {
57+
it('should update value during input', async () => {
58+
const { wrapper, inputValue } = mountNcRichContenteditable()
59+
const TEST_TEXT = 'Test Text'
60+
await inputValue('Test Text')
61+
expect(wrapper.emitted('update:value')).toBeDefined()
62+
expect(wrapper.emitted('update:value').at(-1)[0]).toBe(TEST_TEXT)
63+
})
64+
65+
it('should not emit "submit" during input', async () => {
66+
const { wrapper, inputValue } = mountNcRichContenteditable()
67+
await inputValue('Test Text')
68+
expect(wrapper.emitted('submit')).not.toBeDefined()
69+
})
70+
71+
it('should emit "paste" on past', async () => {
72+
const { wrapper } = mountNcRichContenteditable()
73+
await wrapper.trigger('paste', { clipboardData: { getData: () => 'PASTED_TEXT', files: [], items: {} } })
74+
expect(wrapper.emitted('paste')).toBeDefined()
75+
expect(wrapper.emitted('paste')).toHaveLength(1)
76+
})
77+
78+
it('should emit "submit" on Enter', async () => {
79+
const { wrapper, inputValue } = mountNcRichContenteditable()
80+
81+
await inputValue('Test Text')
82+
83+
await wrapper.trigger('keydown', { keyCode: 13 }) // Enter
84+
85+
expect(wrapper.emitted('submit')).toBeDefined()
86+
expect(wrapper.emitted('submit')).toHaveLength(1)
87+
})
88+
89+
it('should not emit "submit" on Enter during composition session', async () => {
90+
const { wrapper, inputValue } = mountNcRichContenteditable()
91+
92+
await wrapper.trigger('compositionstart')
93+
await inputValue('猫')
94+
await wrapper.trigger('keydown', { keyCode: 13 }) // Enter
95+
await wrapper.trigger('compositionend')
96+
await inputValue(' - means "Cat"')
97+
await wrapper.trigger('keydown', { keyCode: 13 }) // Enter
98+
99+
expect(wrapper.emitted('submit')).toBeDefined()
100+
expect(wrapper.emitted('submit')).toHaveLength(1)
101+
})
102+
103+
it('should proxy component events listeners to native event handlers', async () => {
104+
const handlers = {
105+
focus: jest.fn(),
106+
paste: jest.fn(),
107+
blur: jest.fn(),
108+
}
109+
const { wrapper } = mountNcRichContenteditable({
110+
listeners: handlers,
111+
})
112+
113+
await wrapper.trigger('focus')
114+
await wrapper.trigger('paste', { clipboardData: { getData: () => 'PASTED_TEXT', files: [], items: {} } })
115+
await wrapper.trigger('blur')
116+
117+
expect(handlers.focus).toHaveBeenCalledTimes(1)
118+
expect(handlers.paste).toHaveBeenCalledTimes(1)
119+
expect(handlers.blur).toHaveBeenCalledTimes(1)
120+
})
121+
})

0 commit comments

Comments
 (0)