Compare commits
2 commits
master
...
fix/tab_na
Author | SHA1 | Date | |
---|---|---|---|
d7c469956e | |||
7057722772 |
5
package-lock.json
generated
5
package-lock.json
generated
|
@ -11689,6 +11689,11 @@
|
||||||
"integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=",
|
"integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"tabbable": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ=="
|
||||||
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"version": "5.4.6",
|
"version": "5.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
|
||||||
|
|
|
@ -42,7 +42,8 @@
|
||||||
"v-clipboard": "^2.2.3",
|
"v-clipboard": "^2.2.3",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
"vue-router": "^3.3.4",
|
"vue-router": "^3.3.4",
|
||||||
"vuedraggable": "^2.23.2"
|
"vuedraggable": "^2.23.2",
|
||||||
|
"tabbable": "^4.0.0"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"extends @nextcloud/browserslist-config"
|
"extends @nextcloud/browserslist-config"
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
@keydown.enter.prevent="addNewEntry">
|
@keydown.enter.prevent="addNewEntry">
|
||||||
|
|
||||||
<!-- Delete answer -->
|
<!-- Delete answer -->
|
||||||
<Actions>
|
<Actions @keydown.tab.exact.prevent="onActionsTab">
|
||||||
<ActionButton icon="icon-close" @click="deleteEntry">
|
<ActionButton icon="icon-close" @click="deleteEntry">
|
||||||
{{ t('forms', 'Delete answer') }}
|
{{ t('forms', 'Delete answer') }}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
@ -79,6 +79,11 @@ export default {
|
||||||
this.$refs.input.focus()
|
this.$refs.input.focus()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onActionsTab(event) {
|
||||||
|
console.debug('keydownTab')
|
||||||
|
// this.$emit('focus-next', event.target)
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Option changed, processing the data
|
* Option changed, processing the data
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -25,9 +25,7 @@
|
||||||
:class="{ 'question--edit': edit }"
|
:class="{ 'question--edit': edit }"
|
||||||
:aria-label="t('forms', 'Question number {index}', {index})"
|
:aria-label="t('forms', 'Question number {index}', {index})"
|
||||||
class="question"
|
class="question"
|
||||||
@click="enableEdit"
|
@click="enableEdit">
|
||||||
@focusin="onFocusIn"
|
|
||||||
@focusout="onFocusOut">
|
|
||||||
<!-- Drag handle -->
|
<!-- Drag handle -->
|
||||||
<!-- TODO: implement arrow key mapping to reorder question -->
|
<!-- TODO: implement arrow key mapping to reorder question -->
|
||||||
<div v-if="!readOnly"
|
<div v-if="!readOnly"
|
||||||
|
@ -47,7 +45,8 @@
|
||||||
minlength="1"
|
minlength="1"
|
||||||
:maxlength="maxQuestionLength"
|
:maxlength="maxQuestionLength"
|
||||||
required
|
required
|
||||||
@input="onTitleChange">
|
@input="onTitleChange"
|
||||||
|
@keydown.shift.tab.prevent="onTitleShiftTab">
|
||||||
<h3 v-else
|
<h3 v-else
|
||||||
class="question__header-title"
|
class="question__header-title"
|
||||||
:tabindex="computedTitleTabIndex"
|
:tabindex="computedTitleTabIndex"
|
||||||
|
@ -57,7 +56,10 @@
|
||||||
v-tooltip.auto="warningInvalid"
|
v-tooltip.auto="warningInvalid"
|
||||||
class="question__header-warning icon-error-color"
|
class="question__header-warning icon-error-color"
|
||||||
tabindex="0" />
|
tabindex="0" />
|
||||||
<Actions v-if="!readOnly" class="question__header-menu" :force-menu="true">
|
<Actions v-if="!readOnly"
|
||||||
|
class="question__header-menu"
|
||||||
|
:force-menu="true"
|
||||||
|
@keydown.tab.exact.prevent="onActionsTab">
|
||||||
<ActionCheckbox :checked="mandatory"
|
<ActionCheckbox :checked="mandatory"
|
||||||
@update:checked="onMandatoryChange">
|
@update:checked="onMandatoryChange">
|
||||||
{{ t('forms', 'Required') }}
|
{{ t('forms', 'Required') }}
|
||||||
|
@ -168,6 +170,16 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
onTitleShiftTab(event) {
|
||||||
|
console.debug('this', event.target)
|
||||||
|
this.$emit('focus-prev', event.target)
|
||||||
|
this.disableEdit()
|
||||||
|
},
|
||||||
|
onActionsTab(event) {
|
||||||
|
console.debug('actionsTab', event)
|
||||||
|
this.$emit('focus-next', event.target)
|
||||||
|
},
|
||||||
|
|
||||||
onTitleChange({ target }) {
|
onTitleChange({ target }) {
|
||||||
this.$emit('update:text', target.value)
|
this.$emit('update:text', target.value)
|
||||||
},
|
},
|
||||||
|
@ -188,26 +200,11 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable & disable resp. edit, if focus jumps to next question (e.g. by tab-navigation)
|
|
||||||
* @param {Object} event The triggered focusIn/focusOut event
|
|
||||||
*/
|
|
||||||
onFocusIn(event) {
|
|
||||||
if (event.target.closest('.question') !== event.relatedTarget?.closest('.question')) {
|
|
||||||
this.enableEdit()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onFocusOut(event) {
|
|
||||||
if (event.target.closest('.question') !== event.relatedTarget?.closest('.question')) {
|
|
||||||
this.disableEdit()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable the edit mode
|
* Enable the edit mode
|
||||||
*/
|
*/
|
||||||
enableEdit() {
|
enableEdit() {
|
||||||
|
console.debug('enableEdit')
|
||||||
if (!this.readOnly) {
|
if (!this.readOnly) {
|
||||||
this.$emit('update:edit', true)
|
this.$emit('update:edit', true)
|
||||||
}
|
}
|
||||||
|
@ -217,6 +214,7 @@ export default {
|
||||||
* Disable the edit mode
|
* Disable the edit mode
|
||||||
*/
|
*/
|
||||||
disableEdit() {
|
disableEdit() {
|
||||||
|
console.debug('disableEdit')
|
||||||
if (!this.readOnly) {
|
if (!this.readOnly) {
|
||||||
this.$emit('update:edit', false)
|
this.$emit('update:edit', false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,9 @@
|
||||||
:warning-invalid="answerType.warningInvalid"
|
:warning-invalid="answerType.warningInvalid"
|
||||||
@update:text="onTitleChange"
|
@update:text="onTitleChange"
|
||||||
@update:mandatory="onMandatoryChange"
|
@update:mandatory="onMandatoryChange"
|
||||||
@delete="onDelete">
|
@delete="onDelete"
|
||||||
|
@focus-prev="onFocusPrev"
|
||||||
|
@focus-next="FocusNextOutside">
|
||||||
<div class="question__content">
|
<div class="question__content">
|
||||||
<textarea ref="textarea"
|
<textarea ref="textarea"
|
||||||
:aria-label="t('forms', 'A long answer for the question “{text}”', { text })"
|
:aria-label="t('forms', 'A long answer for the question “{text}”', { text })"
|
||||||
|
|
|
@ -34,13 +34,14 @@
|
||||||
:shift-drag-handle="shiftDragHandle"
|
:shift-drag-handle="shiftDragHandle"
|
||||||
@update:text="onTitleChange"
|
@update:text="onTitleChange"
|
||||||
@update:mandatory="onMandatoryChange"
|
@update:mandatory="onMandatoryChange"
|
||||||
@delete="onDelete">
|
@delete="onDelete"
|
||||||
|
@focus-prev="onFocusPrev"
|
||||||
|
@focus-next="onFocusNext">
|
||||||
<ul class="question__content">
|
<ul class="question__content">
|
||||||
<template v-for="(answer, index) in options">
|
<template v-for="(answer, index) in options">
|
||||||
<li v-if="!edit"
|
<li v-if="!edit"
|
||||||
:key="answer.id"
|
:key="answer.id"
|
||||||
class="question__item"
|
class="question__item">
|
||||||
:class="{'question__item--last': (index === options.length-1),}">
|
|
||||||
<!-- Answer radio/checkbox + label -->
|
<!-- Answer radio/checkbox + label -->
|
||||||
<!-- TODO: migrate to radio/checkbox component once available -->
|
<!-- TODO: migrate to radio/checkbox component once available -->
|
||||||
<input :id="`${id}-answer-${answer.id}`"
|
<input :id="`${id}-answer-${answer.id}`"
|
||||||
|
@ -55,7 +56,8 @@
|
||||||
:required="isRequired(answer.id)"
|
:required="isRequired(answer.id)"
|
||||||
:type="isUnique ? 'radio' : 'checkbox'"
|
:type="isUnique ? 'radio' : 'checkbox'"
|
||||||
@change="onChange($event, answer.id)"
|
@change="onChange($event, answer.id)"
|
||||||
@keydown.enter.exact.prevent="onKeydownEnter">
|
@keydown.enter.exact.prevent="onKeydownEnter"
|
||||||
|
@focus="onFocusRadioInput">
|
||||||
<label v-if="!edit"
|
<label v-if="!edit"
|
||||||
ref="label"
|
ref="label"
|
||||||
:for="`${id}-answer-${answer.id}`"
|
:for="`${id}-answer-${answer.id}`"
|
||||||
|
@ -72,7 +74,8 @@
|
||||||
:max-option-length="maxStringLengths.optionText"
|
:max-option-length="maxStringLengths.optionText"
|
||||||
@add="addNewEntry"
|
@add="addNewEntry"
|
||||||
@delete="deleteOption"
|
@delete="deleteOption"
|
||||||
@update:answer="updateAnswer" />
|
@update:answer="updateAnswer"
|
||||||
|
@focus-next="onFocusNext" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<li v-if="(edit && !isLastEmpty) || hasNoAnswer" class="question__item">
|
<li v-if="(edit && !isLastEmpty) || hasNoAnswer" class="question__item">
|
||||||
|
@ -153,15 +156,6 @@ export default {
|
||||||
|
|
||||||
// update parent
|
// update parent
|
||||||
this.updateOptions(options)
|
this.updateOptions(options)
|
||||||
} else {
|
|
||||||
// If edit becomes true by tabbing to last element, focus newAnswer-input
|
|
||||||
if (document.activeElement?.parentNode?.classList.contains('question__item--last')) {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (this.$refs.inputNewAnswer) {
|
|
||||||
this.$refs.inputNewAnswer.focus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -188,6 +182,20 @@ export default {
|
||||||
this.$emit('update:values', [...new Set(values)])
|
this.$emit('update:values', [...new Set(values)])
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the focus gets onto one of the radio/checkbox inputs and readOnly is false, activate Edit and focus inputNewAnswer
|
||||||
|
* Necessary for Tab-Navigation on editing forms.
|
||||||
|
*/
|
||||||
|
onFocusRadioInput() {
|
||||||
|
console.debug('Inputfocus!')
|
||||||
|
if (!this.readOnly) {
|
||||||
|
this.enableEdit()
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.inputNewAnswer.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is the provided answer checked ?
|
* Is the provided answer checked ?
|
||||||
* @param {number} id the answer id
|
* @param {number} id the answer id
|
||||||
|
|
|
@ -32,7 +32,10 @@
|
||||||
:warning-invalid="answerType.warningInvalid"
|
:warning-invalid="answerType.warningInvalid"
|
||||||
@update:text="onTitleChange"
|
@update:text="onTitleChange"
|
||||||
@update:mandatory="onMandatoryChange"
|
@update:mandatory="onMandatoryChange"
|
||||||
@delete="onDelete">
|
@delete="onDelete"
|
||||||
|
@focus-prev="onFocusPrev"
|
||||||
|
@focus-next="FocusNextOutside"
|
||||||
|
@keydown.tab.exact="onKeydownTab">
|
||||||
<div class="question__content">
|
<div class="question__content">
|
||||||
<input ref="input"
|
<input ref="input"
|
||||||
:aria-label="t('forms', 'A short answer for the question “{text}”', { text })"
|
:aria-label="t('forms', 'A short answer for the question “{text}”', { text })"
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { debounce } from 'debounce'
|
||||||
import { generateUrl } from '@nextcloud/router'
|
import { generateUrl } from '@nextcloud/router'
|
||||||
import { showError } from '@nextcloud/dialogs'
|
import { showError } from '@nextcloud/dialogs'
|
||||||
import axios from '@nextcloud/axios'
|
import axios from '@nextcloud/axios'
|
||||||
|
import tabbable from 'tabbable'
|
||||||
|
|
||||||
import Question from '../components/Questions/Question'
|
import Question from '../components/Questions/Question'
|
||||||
|
|
||||||
|
@ -181,5 +182,42 @@ export default {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable the edit mode
|
||||||
|
*/
|
||||||
|
enableEdit() {
|
||||||
|
console.debug('enableEdit')
|
||||||
|
if (!this.readOnly) {
|
||||||
|
this.edit = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable the edit mode
|
||||||
|
*/
|
||||||
|
disableEdit() {
|
||||||
|
console.debug('disableEdit')
|
||||||
|
if (!this.readOnly) {
|
||||||
|
this.edit = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onFocusPrev(elem) {
|
||||||
|
console.debug('TabPrev Mixin', elem)
|
||||||
|
this.$emit('focus-prev', elem)
|
||||||
|
},
|
||||||
|
onFocusNext(elem) {
|
||||||
|
console.debug('TabNext', elem)
|
||||||
|
this.$emit('focus-next', elem)
|
||||||
|
},
|
||||||
|
FocusNextOutside(elem) {
|
||||||
|
console.debug('focusnextoutside', elem)
|
||||||
|
this.$emit('focus-next', elem)
|
||||||
|
this.disableEdit()
|
||||||
|
},
|
||||||
|
onKeydownTab(event) {
|
||||||
|
console.debug('keydownTab')
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,7 +94,9 @@
|
||||||
:index="index + 1"
|
:index="index + 1"
|
||||||
:max-string-lengths="maxStringLengths"
|
:max-string-lengths="maxStringLengths"
|
||||||
v-bind.sync="question"
|
v-bind.sync="question"
|
||||||
@delete="deleteQuestion(question)" />
|
@delete="deleteQuestion(question)"
|
||||||
|
@focus-prev="onFocusPrev"
|
||||||
|
@focus-next="onFocusNext" />
|
||||||
</Draggable>
|
</Draggable>
|
||||||
|
|
||||||
<!-- Add new questions toolbar -->
|
<!-- Add new questions toolbar -->
|
||||||
|
@ -126,6 +128,7 @@ import { loadState } from '@nextcloud/initial-state'
|
||||||
import { showError } from '@nextcloud/dialogs'
|
import { showError } from '@nextcloud/dialogs'
|
||||||
import axios from '@nextcloud/axios'
|
import axios from '@nextcloud/axios'
|
||||||
import debounce from 'debounce'
|
import debounce from 'debounce'
|
||||||
|
import tabbable from 'tabbable'
|
||||||
import Draggable from 'vuedraggable'
|
import Draggable from 'vuedraggable'
|
||||||
|
|
||||||
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
|
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
|
||||||
|
@ -230,6 +233,22 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
onFocusPrev(elem) {
|
||||||
|
console.debug('Create tabPrev', elem)
|
||||||
|
const tabelements = tabbable(this.$el)
|
||||||
|
console.debug(tabelements)
|
||||||
|
const ind = tabelements.findIndex(tabbableElement => tabbableElement === elem)
|
||||||
|
console.debug(tabelements[ind - 1])
|
||||||
|
tabelements[ind - 1].focus()
|
||||||
|
},
|
||||||
|
onFocusNext(elem) {
|
||||||
|
console.debug('Create tabNext', elem)
|
||||||
|
const tabelements = tabbable(this.$el)
|
||||||
|
console.debug(tabelements)
|
||||||
|
const ind = tabelements.findIndex(tabbableElement => tabbableElement === elem)
|
||||||
|
console.debug(tabelements[ind + 1])
|
||||||
|
tabelements[ind + 1].focus()
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Fetch the full form data and update parent
|
* Fetch the full form data and update parent
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in a new issue