Skip to content

Commit 36dd8cb

Browse files
don9x2Ebackportbot[bot]
authored andcommitted
fix: require explicit intent for option creation (IME safe)
Signed-off-by: don9x2E <revan@kakao.com> [skip ci]
1 parent 05b26ec commit 36dd8cb

File tree

2 files changed

+93
-19
lines changed

2 files changed

+93
-19
lines changed

playwright/e2e/ime-input.spec.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ test.beforeEach(async ({ page }) => {
1717
})
1818

1919
test(
20-
'IME input does not trigger new option',
20+
'IME input requires explicit intent to create new option',
2121
{
2222
annotation: {
2323
type: 'issue',
@@ -75,7 +75,12 @@ test(
7575
await client.send('Input.insertText', {
7676
text: 'さ',
7777
})
78-
// so there were 4 inputs but those should only result in one new option
78+
// Committing composition text alone must not create a new option.
79+
await expect(question.answerInputs).toHaveCount(0)
80+
await expect(question.newAnswerInput).toHaveValue('さ')
81+
82+
// Explicit intent (Enter) creates exactly one option.
83+
await question.newAnswerInput.press('Enter')
7984
await expect(question.answerInputs).toHaveCount(1)
8085
await expect(question.answerInputs).toHaveValue('さ')
8186
},

src/components/Questions/AnswerInput.vue

Lines changed: 86 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@
1111
class="question__item__pseudoInput" />
1212
<input
1313
ref="input"
14+
v-model="localText"
1415
:aria-label="ariaLabel"
1516
:placeholder="placeholder"
16-
:value="answer.text"
1717
class="question__input"
1818
:class="{ 'question__input--shifted': !isDropdown }"
1919
:maxlength="maxOptionLength"
2020
type="text"
2121
dir="auto"
2222
@input="debounceOnInput"
2323
@keydown.delete="deleteEntry"
24-
@keydown.enter.prevent="focusNextInput"
24+
@keydown.enter.prevent="onEnter"
2525
@compositionstart="onCompositionStart"
2626
@compositionend="onCompositionEnd" />
2727

@@ -64,6 +64,17 @@
6464
</template>
6565
</NcButton>
6666
</div>
67+
<div v-else class="option__actions">
68+
<NcButton
69+
:aria-label="t('forms', 'Add a new answer option')"
70+
variant="tertiary"
71+
:disabled="isIMEComposing || !canCreateLocalAnswer"
72+
@click="createLocalAnswer">
73+
<template #icon>
74+
<IconPlus :size="20" />
75+
</template>
76+
</NcButton>
77+
</div>
6778
</li>
6879
</template>
6980

@@ -98,6 +109,7 @@ export default {
98109
IconCheckboxBlankOutline,
99110
IconDelete,
100111
IconDragIndicator,
112+
IconPlus,
101113
IconRadioboxBlank,
102114
NcActions,
103115
NcActionButton,
@@ -140,10 +152,18 @@ export default {
140152
queue: null,
141153
debounceOnInput: null,
142154
isIMEComposing: false,
155+
localText: this.answer?.text ?? '',
143156
}
144157
},
145158
146159
computed: {
160+
canCreateLocalAnswer() {
161+
if (this.answer.local) {
162+
return !!this.localText?.trim()
163+
}
164+
return !!this.answer.text?.trim()
165+
},
166+
147167
ariaLabel() {
148168
if (this.answer.local) {
149169
return t('forms', 'Add a new answer option')
@@ -169,6 +189,17 @@ export default {
169189
},
170190
},
171191
192+
watch: {
193+
// Keep localText in sync when the parent replaces/updates the answer prop
194+
answer: {
195+
handler(newVal) {
196+
this.localText = newVal?.text ?? ''
197+
},
198+
199+
deep: true,
200+
},
201+
},
202+
172203
created() {
173204
this.queue = new PQueue({ concurrency: 1 })
174205
@@ -196,34 +227,72 @@ export default {
196227
* @param {InputEvent} event The input event that triggered adding a new entry
197228
*/
198229
async onInput({ target, isComposing }) {
230+
if (this.answer.local) {
231+
this.localText = target.value
232+
return
233+
}
234+
199235
if (!isComposing && !this.isIMEComposing && target.value !== '') {
200236
// clone answer
201237
const answer = Object.assign({}, this.answer)
202238
answer.text = this.$refs.input.value
203239
204-
if (this.answer.local) {
205-
// Dispatched for creation. Marked as synced
206-
this.$set(this.answer, 'local', false)
207-
const newAnswer = await this.createAnswer(answer)
240+
await this.updateAnswer(answer)
241+
242+
// Forward changes, but use current answer.text to avoid erasing
243+
// any in-between changes while updating the answer
244+
answer.text = this.$refs.input.value
245+
this.$emit('update:answer', this.index, answer)
246+
}
247+
},
208248
209-
// Forward changes, but use current answer.text to avoid erasing
210-
// any in-between changes while creating the answer
211-
newAnswer.text = this.$refs.input.value
249+
/**
250+
* Handle Enter key: create local answer or move focus
251+
*
252+
* @param {KeyboardEvent} e the keydown event
253+
*/
254+
onEnter(e) {
255+
if (this.answer.local) {
256+
this.createLocalAnswer(e)
257+
return
258+
}
259+
this.focusNextInput(e)
260+
},
212261
213-
this.$emit('create-answer', this.index, newAnswer)
214-
} else {
215-
await this.updateAnswer(answer)
262+
/**
263+
* Create a new local answer option from the current input
264+
*
265+
* @param {Event} e the triggering event
266+
*/
267+
async createLocalAnswer(e) {
268+
if (this.isIMEComposing || e?.isComposing) {
269+
return
270+
}
216271
217-
// Forward changes, but use current answer.text to avoid erasing
218-
// any in-between changes while updating the answer
219-
answer.text = this.$refs.input.value
220-
this.$emit('update:answer', this.index, answer)
221-
}
272+
const value = this.localText ?? ''
273+
if (!value.trim()) {
274+
return
222275
}
276+
277+
const answer = { ...this.answer }
278+
answer.text = value
279+
280+
// Dispatched for creation. Marked as synced
281+
this.$set(this.answer, 'local', false)
282+
const newAnswer = await this.createAnswer(answer)
283+
284+
// Forward changes, but use current answer.text to avoid erasing
285+
// any in-between changes while creating the answer
286+
newAnswer.text = this.$refs.input.value
287+
this.localText = ''
288+
289+
this.$emit('create-answer', this.index, newAnswer)
223290
},
224291
225292
/**
226293
* Request a new answer
294+
*
295+
* @param {Event} e the triggering event
227296
*/
228297
focusNextInput(e) {
229298
if (this.isIMEComposing || e?.isComposing) {

0 commit comments

Comments
 (0)