@@ -49,7 +51,7 @@
:filterable="filterable"
:is-disabled="isDisabled"
:query.sync="query"
- @keydown-enter="handleEnterKeydown"
+ @keyup-enter="handleEnterKeyUp"
@focus="handleFocus"
@remove-tag="deleteTag"
@exit="visible = false"
@@ -432,9 +434,51 @@ export default {
if (dropdown?.parentNode === document.body) {
document.body.removeChild(dropdown);
}
+
+ document.removeEventListener('keyup', this.handleKeyUp, true);
},
methods: {
+ togglePopper() {
+ if (this.popper) {
+ this.hidePopper();
+ } else {
+ this.showPopper();
+ }
+ },
+
+ handleKeyUp(e) {
+ if (
+ this.$refs.input.$el.querySelector('input') === e.target &&
+ e.key === 'Enter'
+ ) {
+ this.togglePopper();
+ }
+
+ switch (e.key) {
+ case 'Escape': {
+ this.visible = false;
+ break;
+ }
+ case 'Tab': {
+ if (!this.$refs.reference.contains(document.activeElement)) {
+ this.visible = false;
+ }
+ break;
+ }
+ case 'ArrowRight':
+ case 'ArrowUp':
+ case 'ArrowLeft':
+ case 'ArrowDown':
+ case 'Enter': {
+ this.$refs.dropdown.navigateDropdown(e);
+ break;
+ }
+ default:
+ break;
+ }
+ },
+
handleOutsideClick() {
this.visible = false;
},
@@ -448,13 +492,13 @@ export default {
},
createPopper() {
- const { reference, dropdown } = this.$refs;
+ const { input, dropdown } = this.$refs;
if (this.appendToBody) {
document.body.appendChild(dropdown.$el);
}
- this.popper = createPopper(reference.$el, dropdown.$el, {
+ this.popper = createPopper(input.$el, dropdown.$el, {
modifiers: [
{
name: 'offset',
@@ -469,6 +513,7 @@ export default {
showPopper() {
this.isDropdownShown = true;
this.createPopper();
+ document.addEventListener('keyup', this.handleKeyUp, true);
},
hidePopper() {
@@ -479,6 +524,7 @@ export default {
this.popper.destroy();
this.popper = null;
+ document.removeEventListener('keyup', this.handleKeyUp, true);
},
getKey(value) {
@@ -556,7 +602,7 @@ export default {
blur() {
this.visible = false;
- this.$refs.reference.blur();
+ this.$refs.input.blur();
},
handleBlur(event) {
@@ -565,7 +611,7 @@ export default {
}, 50);
},
- handleClearClick() {
+ clearSelected() {
const value = this.multiple ? [] : null;
this.emitValueUpdate(value);
@@ -619,11 +665,11 @@ export default {
}
if (this.visible) {
- (this.$refs.tags?.$refs.input ?? this.$refs.reference).focus();
+ (this.$refs.tags?.$refs.input ?? this.$refs.input).focus();
}
},
- handleEnterKeydown() {
+ handleEnterKeyUp() {
if (!this.visible) {
this.toggleMenu();
return;
diff --git a/src/qComponents/QSelect/src/QSelectDropdown.vue b/src/qComponents/QSelect/src/QSelectDropdown.vue
index 88c708d5..31408b7c 100644
--- a/src/qComponents/QSelect/src/QSelectDropdown.vue
+++ b/src/qComponents/QSelect/src/QSelectDropdown.vue
@@ -13,11 +13,13 @@
>
@@ -134,6 +136,57 @@ export default {
},
methods: {
+ navigateDropdown(e) {
+ if (
+ ['ArrowDown', 'ArrowUp'].includes(e.key) &&
+ e.target instanceof HTMLInputElement
+ ) {
+ const firstNode = this.$el.querySelector(`.q-option`);
+ firstNode?.focus();
+ }
+
+ if (!e.target.classList.contains('q-option')) return;
+ const availableOptions = this.options.filter(
+ ({ isDisabled, isVisible }) => !isDisabled && isVisible
+ );
+ const availableElements = availableOptions.map(option => option.$el);
+ let currentNodeIndex;
+ let nextNodeIndex;
+ availableElements.forEach((element, index) => {
+ if (document.activeElement === element) {
+ currentNodeIndex = index;
+ }
+ });
+
+ switch (e.key) {
+ case 'ArrowUp':
+ case 'ArrowLeft':
+ nextNodeIndex = currentNodeIndex - 1;
+ break;
+
+ case 'Tab':
+ this.qSelect.visible = false;
+ break;
+
+ case 'ArrowDown':
+ case 'ArrowRight':
+ nextNodeIndex = currentNodeIndex + 1;
+ break;
+
+ case 'Enter': {
+ this.qSelect.toggleOptionSelection(
+ availableOptions[currentNodeIndex]
+ );
+ break;
+ }
+ default:
+ break;
+ }
+
+ const node = availableElements[nextNodeIndex];
+
+ node?.focus();
+ },
handleSelectAllClick() {
if (this.areAllSelected) {
const keysToRemove = this.options
diff --git a/src/qComponents/QSelect/src/QSelectTags.vue b/src/qComponents/QSelect/src/QSelectTags.vue
index 5780bb27..83ea3f5f 100644
--- a/src/qComponents/QSelect/src/QSelectTags.vue
+++ b/src/qComponents/QSelect/src/QSelectTags.vue
@@ -40,9 +40,9 @@
class="q-select-tags__input"
:autocomplete="autocomplete"
@focus="$emit('focus')"
- @keydown.enter.prevent="$emit('keydown-enter')"
- @keydown.esc.stop.prevent="emitExit"
- @keydown.tab="emitExit"
+ @keyup.esc.stop.prevent="emitExit"
+ @keyup.enter.prevent="$emit('keyup-enter')"
+ @keydown.backspace.capture="handleBackspaceKeyDown"
@input="handleInput"
/>
@@ -65,6 +65,12 @@ export default {
},
methods: {
+ handleBackspaceKeyDown() {
+ if (!this.query) {
+ this.$emit('remove-tag', this.selected[this.selected.length - 1]);
+ }
+ },
+
handleTagClose(option) {
this.$emit('remove-tag', option);
},
diff --git a/src/qComponents/QSelect/src/q-select.scss b/src/qComponents/QSelect/src/q-select.scss
index 5ed78d72..6035d9c3 100644
--- a/src/qComponents/QSelect/src/q-select.scss
+++ b/src/qComponents/QSelect/src/q-select.scss
@@ -17,6 +17,7 @@
.q-input__inner {
height: 100%;
+ user-select: none;
}
}
}
diff --git a/src/qComponents/index.js b/src/qComponents/index.js
index 8f487f5a..78ae61eb 100644
--- a/src/qComponents/index.js
+++ b/src/qComponents/index.js
@@ -1,4 +1,5 @@
/* eslint-disable no-underscore-dangle, global-require, no-param-reassign */
+import 'focus-visible';
import { kebabCase, isString } from 'lodash-es';
import vClickOutside from 'v-click-outside';
import { version } from '../../package.json';
diff --git a/yarn.lock b/yarn.lock
index 31a43562..5672638d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7356,6 +7356,11 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
+focus-visible@^5.2.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/focus-visible/-/focus-visible-5.2.0.tgz#3a9e41fccf587bd25dcc2ef045508284f0a4d6b3"
+ integrity sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==
+
follow-redirects@^1.0.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"