diff --git a/playwright/e2e/ime-input.spec.ts b/playwright/e2e/ime-input.spec.ts
index 6bb78a914..c0ef6aa54 100644
--- a/playwright/e2e/ime-input.spec.ts
+++ b/playwright/e2e/ime-input.spec.ts
@@ -17,7 +17,7 @@ test.beforeEach(async ({ page }) => {
})
test(
- 'IME input does not trigger new option',
+ 'IME input requires explicit intent to create new option',
{
annotation: {
type: 'issue',
@@ -75,7 +75,12 @@ test(
await client.send('Input.insertText', {
text: 'さ',
})
- // so there were 4 inputs but those should only result in one new option
+ // Committing composition text alone must not create a new option.
+ await expect(question.answerInputs).toHaveCount(0)
+ await expect(question.newAnswerInput).toHaveValue('さ')
+
+ // Explicit intent (Enter) creates exactly one option.
+ await question.newAnswerInput.press('Enter')
await expect(question.answerInputs).toHaveCount(1)
await expect(question.answerInputs).toHaveValue('さ')
},
diff --git a/src/components/Questions/AnswerInput.vue b/src/components/Questions/AnswerInput.vue
index 426536ba3..9caa02b2c 100644
--- a/src/components/Questions/AnswerInput.vue
+++ b/src/components/Questions/AnswerInput.vue
@@ -21,7 +21,7 @@
dir="auto"
@input="debounceOnInput"
@keydown.delete="deleteEntry"
- @keydown.enter.prevent="focusNextInput"
+ @keydown.enter.prevent="onEnter"
@compositionstart="onCompositionStart"
@compositionend="onCompositionEnd" />
@@ -64,6 +64,17 @@
+
+
+
+
+
+
+
@@ -98,6 +109,7 @@ export default {
IconCheckboxBlankOutline,
IconDelete,
IconDragIndicator,
+ IconPlus,
IconRadioboxBlank,
IconTableColumn,
IconTableRow,
@@ -167,6 +179,10 @@ export default {
},
computed: {
+ canCreateLocalAnswer() {
+ return !!this.$refs.input?.value?.trim()
+ },
+
ariaLabel() {
if (this.answer.local) {
if (this.optionType === OptionType.Column) {
@@ -266,30 +282,65 @@ export default {
* @param {InputEvent} event The input event that triggered adding a new entry
*/
async onInput({ target, isComposing }) {
+ if (this.answer.local) {
+ this.$set(this.answer, 'text', target.value)
+ return
+ }
+
if (!isComposing && !this.isIMEComposing && target.value !== '') {
// clone answer
const answer = { ...this.answer }
answer.text = this.$refs.input.value
- if (this.answer.local) {
- // Dispatched for creation. Marked as synced
- this.$set(this.answer, 'local', false)
- const newAnswer = await this.createAnswer(answer)
+ await this.updateAnswer(answer)
- // Forward changes, but use current answer.text to avoid erasing
- // any in-between changes while creating the answer
- newAnswer.text = this.$refs.input.value
+ // Forward changes, but use current answer.text to avoid erasing
+ // any in-between changes while updating the answer
+ answer.text = this.$refs.input.value
+ this.$emit('update:answer', this.index, answer)
+ }
+ },
- this.$emit('create-answer', this.index, newAnswer)
- } else {
- await this.updateAnswer(answer)
+ /**
+ * Handle Enter key: create local answer or move focus
+ *
+ * @param {KeyboardEvent} e the keydown event
+ */
+ onEnter(e) {
+ if (this.answer.local) {
+ this.createLocalAnswer(e)
+ return
+ }
+ this.focusNextInput(e)
+ },
- // Forward changes, but use current answer.text to avoid erasing
- // any in-between changes while updating the answer
- answer.text = this.$refs.input.value
- this.$emit('update:answer', this.index, answer)
- }
+ /**
+ * Create a new local answer option from the current input
+ *
+ * @param {Event} e the triggering event
+ */
+ async createLocalAnswer(e) {
+ if (this.isIMEComposing || e?.isComposing) {
+ return
+ }
+
+ const value = this.$refs.input?.value ?? ''
+ if (!value.trim()) {
+ return
}
+
+ const answer = { ...this.answer }
+ answer.text = value
+
+ // Dispatched for creation. Marked as synced
+ this.$set(this.answer, 'local', false)
+ const newAnswer = await this.createAnswer(answer)
+
+ // Forward changes, but use current answer.text to avoid erasing
+ // any in-between changes while creating the answer
+ newAnswer.text = this.$refs.input.value
+
+ this.$emit('create-answer', this.index, newAnswer)
},
/**
diff --git a/src/mixins/QuestionMultipleMixin.ts b/src/mixins/QuestionMultipleMixin.ts
index 0b3a882da..c3df7e78d 100644
--- a/src/mixins/QuestionMultipleMixin.ts
+++ b/src/mixins/QuestionMultipleMixin.ts
@@ -176,7 +176,8 @@ export default defineComponent({
*/
onCreateAnswer(index: number, answer: FormsOption): void {
this.$nextTick(() => {
- this.$nextTick(() => this.focusIndex(index, answer.optionType))
+ // Move focus to the newly appended empty local option.
+ this.$nextTick(() => this.focusIndex(index + 1, answer.optionType))
})
this.updateOptions([...this.options, answer])
},