Skip to content

Commit e41428e

Browse files
committed
Fix option creation flow during IME composition
The previous IME fix still allowed option-creation side effects from input events and did not fully protect Enter-driven actions during composition. This change switches local option creation to explicit user intent (Enter or add button after composition), keeps updates for existing options, and guards Enter/delete handlers with composition checks. It also moves focus to the next empty local option after creating one, and renders the delete-form confirmation dialog outside the actions slot so it opens immediately when clicked. Signed-off-by: don9x2E <revan@kakao.com>
1 parent 66d0be7 commit e41428e

3 files changed

Lines changed: 71 additions & 28 deletions

File tree

src/components/AppNavigationForm.vue

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
-->
55

66
<template>
7-
<NcListItem
7+
<div>
8+
<NcListItem
89
:active="isActive"
910
:actions-aria-label="t('forms', 'Form actions')"
1011
:counter-number="form.submissionCount"
@@ -91,17 +92,18 @@
9192
</template>
9293
{{ t('forms', 'Delete form') }}
9394
</NcActionButton>
94-
<NcDialog
95-
:open.sync="showDeleteDialog"
96-
:name="t('forms', 'Delete form')"
97-
:message="
98-
t('forms', 'Are you sure you want to delete {title}?', {
99-
title: formTitle,
100-
})
101-
"
102-
:buttons="buttons" />
10395
</template>
104-
</NcListItem>
96+
</NcListItem>
97+
<NcDialog
98+
:open.sync="showDeleteDialog"
99+
:name="t('forms', 'Delete form')"
100+
:message="
101+
t('forms', 'Are you sure you want to delete {title}?', {
102+
title: formTitle,
103+
})
104+
"
105+
:buttons="buttons" />
106+
</div>
105107
</template>
106108

107109
<script>

src/components/Questions/AnswerInput.vue

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
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

@@ -167,6 +178,10 @@ export default {
167178
},
168179
169180
computed: {
181+
canCreateLocalAnswer() {
182+
return !!this.$refs.input?.value?.trim()
183+
},
184+
170185
ariaLabel() {
171186
if (this.answer.local) {
172187
if (this.optionType === OptionType.Column) {
@@ -266,30 +281,55 @@ export default {
266281
* @param {InputEvent} event The input event that triggered adding a new entry
267282
*/
268283
async onInput({ target, isComposing }) {
284+
if (this.answer.local) {
285+
this.$set(this.answer, 'text', target.value)
286+
return
287+
}
288+
269289
if (!isComposing && !this.isIMEComposing && target.value !== '') {
270290
// clone answer
271291
const answer = { ...this.answer }
272292
answer.text = this.$refs.input.value
273293
274-
if (this.answer.local) {
275-
// Dispatched for creation. Marked as synced
276-
this.$set(this.answer, 'local', false)
277-
const newAnswer = await this.createAnswer(answer)
294+
await this.updateAnswer(answer)
278295
279-
// Forward changes, but use current answer.text to avoid erasing
280-
// any in-between changes while creating the answer
281-
newAnswer.text = this.$refs.input.value
296+
// Forward changes, but use current answer.text to avoid erasing
297+
// any in-between changes while updating the answer
298+
answer.text = this.$refs.input.value
299+
this.$emit('update:answer', this.index, answer)
300+
}
301+
},
282302
283-
this.$emit('create-answer', this.index, newAnswer)
284-
} else {
285-
await this.updateAnswer(answer)
303+
onEnter(e) {
304+
if (this.answer.local) {
305+
this.createLocalAnswer(e)
306+
return
307+
}
308+
this.focusNextInput(e)
309+
},
286310
287-
// Forward changes, but use current answer.text to avoid erasing
288-
// any in-between changes while updating the answer
289-
answer.text = this.$refs.input.value
290-
this.$emit('update:answer', this.index, answer)
291-
}
311+
async createLocalAnswer(e) {
312+
if (this.isIMEComposing || e?.isComposing) {
313+
return
292314
}
315+
316+
const value = this.$refs.input?.value ?? ''
317+
if (!value.trim()) {
318+
return
319+
}
320+
321+
const answer = { ...this.answer }
322+
answer.text = value
323+
324+
// Dispatched for creation. Marked as synced
325+
this.$set(this.answer, 'local', false)
326+
const newAnswer = await this.createAnswer(answer)
327+
328+
// Forward changes, but use current answer.text to avoid erasing
329+
// any in-between changes while creating the answer
330+
newAnswer.text = this.$refs.input.value
331+
332+
this.$emit('create-answer', this.index, newAnswer)
293333
},
294334
295335
/**

src/mixins/QuestionMultipleMixin.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,8 @@ export default defineComponent({
176176
*/
177177
onCreateAnswer(index: number, answer: FormsOption): void {
178178
this.$nextTick(() => {
179-
this.$nextTick(() => this.focusIndex(index, answer.optionType))
179+
// Move focus to the newly appended empty local option.
180+
this.$nextTick(() => this.focusIndex(index + 1, answer.optionType))
180181
})
181182
this.updateOptions([...this.options, answer])
182183
},

0 commit comments

Comments
 (0)