Skip to content

Commit

Permalink
Merge pull request #1532 from nextcloud/fix/drag-a11y
Browse files Browse the repository at this point in the history
Allow reordering questions using the keyboard
  • Loading branch information
susnux authored Oct 24, 2023
2 parents bf9caed + 0a421da commit 431914e
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 41 deletions.
80 changes: 74 additions & 6 deletions src/components/Questions/Question.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,41 @@

<template>
<li v-click-outside="disableEdit"
:class="{ 'question--edit': edit }"
:class="{
'question': true,
'question--edit': edit,
'question--editable': !readOnly
}"
:aria-label="t('forms', 'Question number {index}', {index})"
class="question"
@click="enableEdit">
<!-- Drag handle -->
<!-- TODO: implement arrow key mapping to reorder question -->
<div v-if="!readOnly"
class="question__drag-handle"
:class="{'question__drag-handle--shiftup': shiftDragHandle}"
:aria-label="t('forms', 'Drag to reorder the questions')">
:class="{
'question__drag-handle': true,
'question__drag-handle--shiftup': shiftDragHandle
}">
<NcButton ref="buttonUp"
:aria-label="t('forms', 'Move question up')"
:disabled="!canMoveUp"
class="question__drag-handle-button"
type="tertiary-no-background"
@click.stop="onMoveUp">
<template #icon>
<IconArrowUp :size="20" />
</template>
</NcButton>
<IconDragHorizontalVariant :size="20" />
<NcButton ref="buttonDown"
:aria-label="t('forms', 'Move question down')"
:disabled="!canMoveDown"
class="question__drag-handle-button"
type="tertiary-no-background"
@click.stop="onMoveDown">
<template #icon>
<IconArrowDown :size="20" />
</template>
</NcButton>
</div>

<!-- Header -->
Expand Down Expand Up @@ -113,8 +137,11 @@ 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'
import NcActionInput from '@nextcloud/vue/dist/Components/NcActionInput.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import IconAlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
import IconArrowDown from 'vue-material-design-icons/ArrowDown.vue'
import IconArrowUp from 'vue-material-design-icons/ArrowUp.vue'
import IconDelete from 'vue-material-design-icons/Delete.vue'
import IconDragHorizontalVariant from 'vue-material-design-icons/DragHorizontalVariant.vue'
import IconIdentifier from 'vue-material-design-icons/Identifier.vue'
Expand All @@ -128,13 +155,16 @@ export default {
components: {
IconAlertCircleOutline,
IconArrowDown,
IconArrowUp,
IconDelete,
IconDragHorizontalVariant,
IconIdentifier,
NcActions,
NcActionButton,
NcActionCheckbox,
NcActionInput,
NcButton,
},
inject: ['$markdownit'],
Expand Down Expand Up @@ -188,6 +218,14 @@ export default {
type: String,
default: t('forms', 'This question needs a title!'),
},
canMoveDown: {
type: Boolean,
default: false,
},
canMoveUp: {
type: Boolean,
default: false,
},
},
computed: {
Expand Down Expand Up @@ -269,6 +307,18 @@ export default {
})
},
/**
* Reorder question but keep focus on the button
*/
onMoveDown() {
this.$emit('move-down')
this.$nextTick(() => this.$refs.buttonDown.$el.focus())
},
onMoveUp() {
this.$emit('move-up')
this.$nextTick(() => this.$refs.buttonUp.$el.focus())
},
/**
* Enable the edit mode
*/
Expand Down Expand Up @@ -311,6 +361,10 @@ export default {
user-select: none;
background-color: var(--color-main-background);
&--editable {
padding-inline-start: 56px; // add 12px for the title input box
}
> * {
cursor: pointer;
}
Expand All @@ -319,19 +373,33 @@ export default {
position: absolute;
display: flex;
inset-inline-start: 0;
flex-direction: column;
justify-content: center;
gap: 12px;
width: 44px;
height: 100%;
opacity: .5;
cursor: grab;
&-button {
position: absolute;
inset-block-start: -9999px;
}
// Avoid moving drag-handle due to newAnswer-input on multiple-Questions
&--shiftup {
height: calc(100% - 44px);
}
&:hover,
&:focus {
&:focus,
&:focus-within {
opacity: 1;
.question__drag-handle-button {
position: initial;
inset-block-start: initial;
}
}
&:active {
Expand Down
6 changes: 1 addition & 5 deletions src/components/Questions/QuestionDate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,7 @@
:max-string-lengths="maxStringLengths"
:title-placeholder="answerType.titlePlaceholder"
:warning-invalid="answerType.warningInvalid"
@update:text="onTitleChange"
@update:description="onDescriptionChange"
@update:isRequired="onRequiredChange"
@update:name="onNameChange"
@delete="onDelete">
v-on="commonListeners">
<div class="question__content">
<NcDatetimePicker v-model="time"
:disabled="!readOnly"
Expand Down
6 changes: 1 addition & 5 deletions src/components/Questions/QuestionDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,7 @@
:warning-invalid="answerType.warningInvalid"
:content-valid="contentValid"
:shift-drag-handle="shiftDragHandle"
@update:text="onTitleChange"
@update:description="onDescriptionChange"
@update:isRequired="onRequiredChange"
@update:name="onNameChange"
@delete="onDelete">
v-on="commonListeners">
<template #actions>
<NcActionCheckbox :checked="extraSettings?.shuffleOptions"
@update:checked="onShuffleOptionsChange">
Expand Down
6 changes: 1 addition & 5 deletions src/components/Questions/QuestionLong.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,7 @@
:max-string-lengths="maxStringLengths"
:title-placeholder="answerType.titlePlaceholder"
:warning-invalid="answerType.warningInvalid"
@update:text="onTitleChange"
@update:description="onDescriptionChange"
@update:isRequired="onRequiredChange"
@update:name="onNameChange"
@delete="onDelete">
v-on="commonListeners">
<div class="question__content">
<textarea ref="textarea"
:aria-label="t('forms', 'A long answer for the question “{text}”', { text })"
Expand Down
6 changes: 1 addition & 5 deletions src/components/Questions/QuestionMultiple.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,7 @@
:warning-invalid="answerType.warningInvalid"
:content-valid="contentValid"
:shift-drag-handle="shiftDragHandle"
@update:text="onTitleChange"
@update:description="onDescriptionChange"
@update:isRequired="onRequiredChange"
@update:name="onNameChange"
@delete="onDelete">
v-on="commonListeners">
<template #actions>
<NcActionCheckbox :checked="extraSettings?.shuffleOptions"
@update:checked="onShuffleOptionsChange">
Expand Down
6 changes: 1 addition & 5 deletions src/components/Questions/QuestionShort.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,7 @@
:max-string-lengths="maxStringLengths"
:title-placeholder="answerType.titlePlaceholder"
:warning-invalid="answerType.warningInvalid"
@update:text="onTitleChange"
@update:description="onDescriptionChange"
@update:isRequired="onRequiredChange"
@update:name="onNameChange"
@delete="onDelete">
v-on="commonListeners">
<div class="question__content">
<input ref="input"
:aria-label="t('forms', 'A short answer for the question “{text}”', { text })"
Expand Down
39 changes: 39 additions & 0 deletions src/mixins/QuestionMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ export default {
required: true,
},

/**
* ID of the form
*/
formId: {
type: Number,
default: null,
},

/**
* The question title
*/
Expand Down Expand Up @@ -90,6 +98,22 @@ export default {
required: true,
},

/**
* Order of the question
*/
order: {
type: Number,
default: -1,
},

/**
* Question type
*/
type: {
type: String,
default: null,
},

/**
* Answer type model object
*/
Expand Down Expand Up @@ -144,6 +168,21 @@ export default {
// Ensure order of options always is the same
return [...this.options].sort((a, b) => a.id - b.id)
},

/**
* Listeners for all questions to forward
*/
commonListeners() {
return {
delete: this.onDelete,
'update:text': this.onTitleChange,
'update:description': this.onDescriptionChange,
'update:isRequired': this.onRequiredChange,
'update:name': this.onNameChange,
'move-down': (...args) => this.$emit('move-down', ...args),
'move-up': (...args) => this.$emit('move-up', ...args),
}
},
},

methods: {
Expand Down
45 changes: 35 additions & 10 deletions src/views/Create.vue
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,21 @@
@change="onQuestionOrderChange"
@start="isDragging = true"
@end="isDragging = false">
<Questions :is="answerTypes[question.type].component"
v-for="(question, index) in form.questions"
ref="questions"
:key="question.id"
:answer-type="answerTypes[question.type]"
:index="index + 1"
:max-string-lengths="maxStringLengths"
v-bind.sync="form.questions[index]"
@delete="deleteQuestion(question)" />
<transition-group :name="isDragging ? 'no-external-transition-on-drag' : 'question-list'">
<component :is="answerTypes[question.type].component"
v-for="(question, index) in form.questions"
ref="questions"
:key="question.id"
:can-move-down="index < (form.questions.length - 1)"
:can-move-up="index > 0"
:answer-type="answerTypes[question.type]"
:index="index + 1"
:max-string-lengths="maxStringLengths"
v-bind.sync="form.questions[index]"
@delete="deleteQuestion(question)"
@move-down="onMoveDown(index)"
@move-up="onMoveUp(index)" />
</transition-group>
</Draggable>
<!-- Add new questions menu -->
Expand Down Expand Up @@ -278,6 +284,18 @@ export default {
},
methods: {
onMoveUp(index) {
if (index > 0) {
[this.form.questions[index - 1], this.form.questions[index]] = [this.form.questions[index], this.form.questions[index - 1]]
this.onQuestionOrderChange()
}
},
onMoveDown(index) {
// only if not the last one
if (index < (this.form.questions.length - 1)) {
this.onMoveUp(index + 1)
}
},
onTitleChange() {
this.resizeTitle()
this.saveTitle()
Expand Down Expand Up @@ -451,6 +469,12 @@ export default {
}
</script>
<style lang="scss">
.question-list-move {
transition: all 0.2s ease;
}
</style>
<style lang="scss" scoped>
@import '../scssmixins/markdownOutput';
Expand All @@ -469,8 +493,9 @@ export default {
header {
display: flex;
flex-direction: column;
margin: 0;
margin-block-end: 24px;
margin-inline-start: 56px;
padding-inline-start: 40px;
.form-title {
font-size: 28px;
Expand Down

0 comments on commit 431914e

Please sign in to comment.