Skip to content

Commit

Permalink
Merge pull request #1750 from nextcloud/fix/a11y-edit-form
Browse files Browse the repository at this point in the history
fix: Make form editable with keyboard
  • Loading branch information
Chartman123 committed Nov 12, 2023
2 parents 6f47ab5 + d99e3b1 commit e1b980b
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 233 deletions.
21 changes: 13 additions & 8 deletions src/components/Questions/AnswerInput.vue
@@ -1,5 +1,5 @@
<template>
<li class="question__item">
<li class="question__item" @focusout="handleTabbing">
<div :is="pseudoIcon"
v-if="!isDropdown"
class="question__item__pseudoInput" />
Expand All @@ -15,7 +15,7 @@
dir="auto"
@input="onInput"
@keydown.delete="deleteEntry"
@keydown.enter.prevent="addNewEntry">
@keydown.enter.prevent="focusNextInput">

<!-- Delete answer -->
<NcActions>
Expand Down Expand Up @@ -98,6 +98,10 @@ export default {
},
methods: {
handleTabbing() {
this.$emit('tabbed-out')
},
/**
* Focus the input
*/
Expand Down Expand Up @@ -133,8 +137,8 @@ export default {
/**
* Request a new answer
*/
addNewEntry() {
this.$emit('add')
focusNextInput() {
this.$emit('focus-next', this.index)
},
/**
Expand Down Expand Up @@ -221,13 +225,14 @@ export default {
.question__input {
width: 100%;
position: relative;
margin-inline-end: 2px !important;
inset-inline-start: -12px;
margin-inline-end: -12px !important;
&--shifted {
inset-inline-start: -30px;
inset-inline-start: -34px;
inset-block-start: 1px;
margin-inline-end: -30px !important;
padding-inline-start: 32px !important;
margin-inline-end: -34px !important;
padding-inline-start: 36px !important;
}
}
}
Expand Down
50 changes: 6 additions & 44 deletions src/components/Questions/Question.vue
Expand Up @@ -21,14 +21,11 @@
-->

<template>
<li v-click-outside="disableEdit"
:class="{
<li :class="{
'question': true,
'question--edit': edit,
'question--editable': !readOnly
}"
:aria-label="t('forms', 'Question number {index}', {index})"
@click="enableEdit">
:aria-label="t('forms', 'Question number {index}', {index})">
<!-- Drag handle -->
<!-- TODO: implement arrow key mapping to reorder question -->
<div v-if="!readOnly"
Expand Down Expand Up @@ -62,7 +59,7 @@
<!-- Header -->
<div class="question__header">
<div class="question__header__title">
<input v-if="edit || !questionValid"
<input v-if="!readOnly || !questionValid"
:placeholder="titlePlaceholder"
:aria-label="t('forms', 'Title of question number {index}', {index})"
:value="text"
Expand All @@ -79,7 +76,7 @@
dir="auto">
{{ computedText }}
</h3>
<div v-if="!edit && !questionValid"
<div v-if="!readOnly && !questionValid"
v-tooltip.auto="warningInvalid"
class="question__header__title__warning"
tabindex="0">
Expand Down Expand Up @@ -111,8 +108,8 @@
</NcActionButton>
</NcActions>
</div>
<div v-if="hasDescription || edit || !questionValid" class="question__header__description">
<textarea v-if="edit || !questionValid"
<div v-if="hasDescription || !readOnly || !questionValid" class="question__header__description">
<textarea v-if="!readOnly || !questionValid"
ref="description"
dir="auto"
:value="description"
Expand All @@ -131,8 +128,6 @@
</template>

<script>
import { directive as ClickOutside } from 'v-click-outside'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcActionCheckbox from '@nextcloud/vue/dist/Components/NcActionCheckbox.js'
Expand All @@ -149,10 +144,6 @@ import IconIdentifier from 'vue-material-design-icons/Identifier.vue'
export default {
name: 'Question',
directives: {
ClickOutside,
},
components: {
IconAlertCircleOutline,
IconArrowDown,
Expand Down Expand Up @@ -194,10 +185,6 @@ export default {
type: Boolean,
default: false,
},
edit: {
type: Boolean,
required: true,
},
readOnly: {
type: Boolean,
default: false,
Expand Down Expand Up @@ -266,13 +253,6 @@ export default {
return this.description !== ''
},
},
watch: {
edit(newEdit) {
if (newEdit || !this.questionValid) {
this.resizeDescription()
}
},
},
// Ensure description is sized correctly on initial render
mounted() {
this.$nextTick(() => this.resizeDescription())
Expand Down Expand Up @@ -319,24 +299,6 @@ export default {
this.$nextTick(() => this.$refs.buttonUp.$el.focus())
},
/**
* Enable the edit mode
*/
enableEdit() {
if (!this.readOnly) {
this.$emit('update:edit', true)
}
},
/**
* Disable the edit mode
*/
disableEdit() {
if (!this.readOnly) {
this.$emit('update:edit', false)
}
},
/**
* Delete this question
*/
Expand Down
4 changes: 4 additions & 0 deletions src/components/Questions/QuestionDate.vue
Expand Up @@ -128,6 +128,10 @@ export default {
.mx-datepicker {
width: 300px;
&.disabled {
inset-inline-start: -12px;
}
:deep(.mx-input) {
height: 44px !important;
}
Expand Down
77 changes: 44 additions & 33 deletions src/components/Questions/QuestionDropdown.vue
Expand Up @@ -33,7 +33,7 @@
{{ t('forms', 'Shuffle options') }}
</NcActionCheckbox>
</template>
<NcSelect v-if="!edit"
<NcSelect v-if="readOnly"
v-model="selectedOption"
:name="name || undefined"
:placeholder="selectOptionPlaceholder"
Expand All @@ -44,7 +44,7 @@
label="text"
@input="onInput" />
<ol v-if="edit" class="question__content">
<ol v-if="!readOnly" class="question__content">
<!-- Answer text input edit -->
<AnswerInput v-for="(answer, index) in options"
:key="index /* using index to keep the same vnode after new answer creation */"
Expand All @@ -54,19 +54,21 @@
:is-unique="!isMultiple"
:is-dropdown="true"
:max-option-length="maxStringLengths.optionText"
@add="addNewEntry"
@delete="deleteOption"
@update:answer="updateAnswer" />
@update:answer="updateAnswer"
@focus-next="focusNextInput"
@tabbed-out="checkValidOption" />
<li v-if="!isLastEmpty || hasNoAnswer" class="question__item">
<input :aria-label="t('forms', 'Add a new answer')"
<input ref="pseudoInput"
v-model="inputValue"
:aria-label="t('forms', 'Add a new answer')"
:placeholder="t('forms', 'Add a new answer')"
class="question__input"
:maxlength="maxStringLengths.optionText"
minlength="1"
type="text"
@click="addNewEntry"
@focus="addNewEntry">
@input="addNewEntry">
</li>
</ol>
</Question>
Expand Down Expand Up @@ -99,6 +101,7 @@ export default {
data() {
return {
selectedOption: null,
inputValue: '',
}
},
Expand Down Expand Up @@ -133,7 +136,7 @@ export default {
},
shiftDragHandle() {
return this.edit && this.options.length !== 0 && !this.isLastEmpty
return !this.readOnly && this.options.length !== 0 && !this.isLastEmpty
},
},
Expand All @@ -146,26 +149,6 @@ export default {
},
methods: {
/**
* Handle toggling the edit mode
* @param {boolean} enabled Whether the edit mode is enabled
*/
onUpdateEdit(enabled) {
// When leaving edit mode, filter and delete empty options
if (!enabled) {
const options = this.options.filter(option => {
if (!option.text) {
this.deleteOptionFromDatabase(option)
return false
}
return true
})
// update parent
this.updateOptions(options)
}
},
onInput(option) {
if (Array.isArray(option)) {
this.$emit('update:values', [...new Set(option.map((opt) => opt.id))])
Expand All @@ -176,6 +159,31 @@ export default {
this.$emit('update:values', option ? [option.id] : [])
},
/**
* Remove any empty options when leaving an option
*/
checkValidOption() {
// When leaving edit mode, filter and delete empty options
this.options.forEach(option => {
if (!option.text) {
this.deleteOption(option.id)
}
})
},
/**
* Set focus on next AnswerInput
*
* @param {number} index Index of current option
*/
focusNextInput(index) {
if (index < this.options.length - 1) {
this.$refs.input[index + 1].focus()
} else if (!this.isLastEmpty || this.hasNoAnswer) {
this.$refs.pseudoInput.focus()
}
},
/**
* Update the options
*
Expand Down Expand Up @@ -204,23 +212,25 @@ export default {
* Add a new empty answer locally
*/
addNewEntry() {
// If entering from non-edit-mode (possible by click), activate edit-mode
this.edit = true
// Add local entry
const options = this.options.slice()
options.push({
id: GenRandomId(),
questionId: this.id,
text: '',
text: this.inputValue,
local: true,
})
this.inputValue = ''
// Update question
this.updateOptions(options)
this.$nextTick(() => {
this.focusIndex(options.length - 1)
// Trigger onInput on new AnswerInput for posting the new option to the API
this.$refs.input[options.length - 1].onInput()
})
},
Expand Down Expand Up @@ -320,7 +330,8 @@ export default {
.question__input {
width: 100%;
position: relative;
margin-inline-end: 46px !important;
inset-inline-start: -12px;
margin-inline-end: 32px !important;
}
}
</style>
2 changes: 2 additions & 0 deletions src/components/Questions/QuestionLong.vue
Expand Up @@ -98,6 +98,8 @@ export default {
// Just overrides Server CSS-Styling for disabled inputs. -> Not Good??
background-color: var(--color-main-background);
color: var(--color-main-text);
width: calc(100% - 32px) !important;
margin-inline-start: -12px;
}
}
Expand Down

0 comments on commit e1b980b

Please sign in to comment.