Skip to content

Commit

Permalink
Merge pull request #4501 from nextcloud-libraries/fix/stop-keyboard-n…
Browse files Browse the repository at this point in the history
…avigation

Stop propagation of keyboard navigation in a number of components
  • Loading branch information
ShGKme committed Sep 7, 2023
2 parents 704a6cd + 2cb60f2 commit 365cbad
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 17 deletions.
Expand Up @@ -30,7 +30,7 @@
<template>
<div class="app-navigation-input-confirm">
<form @submit.prevent="confirm"
@keydown.esc.exact.prevent="cancel"
@keydown.esc.exact.stop.prevent="cancel"
@click.stop.prevent>
<input ref="input"
v-model="valueModel"
Expand Down
2 changes: 1 addition & 1 deletion src/components/NcAppSidebar/NcAppSidebar.vue
Expand Up @@ -418,7 +418,7 @@ export default {
type="text"
:placeholder="namePlaceholder"
:value="name"
@keydown.esc="onDismissEditing"
@keydown.esc.stop="onDismissEditing"
@input="onNameInput">
<NcButton type="tertiary-no-background"
:aria-label="changeNameTranslated"
Expand Down
14 changes: 7 additions & 7 deletions src/components/NcAppSidebar/NcAppSidebarTabs.vue
Expand Up @@ -31,13 +31,13 @@
<div v-if="hasMultipleTabs"
role="tablist"
class="app-sidebar-tabs__nav"
@keydown.left.exact.prevent="focusPreviousTab"
@keydown.right.exact.prevent="focusNextTab"
@keydown.tab.exact.prevent="focusActiveTabContent"
@keydown.home.exact.prevent="focusFirstTab"
@keydown.end.exact.prevent="focusLastTab"
@keydown.33.exact.prevent="focusFirstTab"
@keydown.34.exact.prevent="focusLastTab">
@keydown.left.exact.prevent.stop="focusPreviousTab"
@keydown.right.exact.prevent.stop="focusNextTab"
@keydown.tab.exact.prevent.stop="focusActiveTabContent"
@keydown.home.exact.prevent.stop="focusFirstTab"
@keydown.end.exact.prevent.stop="focusLastTab"
@keydown.33.exact.prevent.stop="focusFirstTab"
@keydown.34.exact.prevent.stop="focusLastTab">
<NcCheckboxRadioSwitch v-for="tab in tabs"
:key="tab.id"
:aria-controls="`tab-${tab.id}`"
Expand Down
40 changes: 35 additions & 5 deletions src/components/NcModal/NcModal.vue
Expand Up @@ -73,14 +73,30 @@ export default {
<NcButton @click="showModal">Show Modal with fields</NcButton>
<NcModal
v-if="modal"
ref="modalRef"
@close="closeModal"
name="Name inside modal">
<div class="modal__content">
<h2>Please enter your name</h2>
<NcTextField label="First Name" :value.sync="firstName" />
<NcTextField label="Last Name" :value.sync="lastName" />
<div class="form-group">
<NcTextField label="First Name" :value.sync="firstName" />
</div>
<div class="form-group">
<NcTextField label="Last Name" :value.sync="lastName" />
</div>
<div class="form-group">
<label for="pizza">What is the most important pizza item?</label>
<NcSelect input-id="pizza" :options="['Cheese', 'Tomatos', 'Pineapples']" v-model="pizza" />
</div>
<div class="form-group">
<label for="emoji-trigger">Select your favorite emoji</label>
<NcEmojiPicker v-if="modalRef" :container="modalRef.$el">
<NcButton id="emoji-trigger">Select</NcButton>
</NcEmojiPicker>
</div>

<NcButton
:disabled="!this.firstName || !this.lastName"
:disabled="!firstName || !lastName || !pizza"
@click="closeModal"
type="primary">
Submit
Expand All @@ -90,12 +106,20 @@ export default {
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
return {
modalRef: ref(null),
}
},
data() {
return {
modal: false,
firstName: '',
lastName: '',
pizza: [],
}
},
methods: {
Expand All @@ -113,11 +137,17 @@ export default {
<style scoped>
.modal__content {
margin: 50px;
}
.modal__content h2 {
text-align: center;
}
.input-field {
margin: 12px 0px;
.form-group {
margin: calc(var(--default-grid-baseline) * 4) 0;
display: flex;
flex-direction: column;
align-items: flex-start;
}
</style>
```
Expand Down
41 changes: 40 additions & 1 deletion src/components/NcPopover/NcPopover.vue
Expand Up @@ -165,9 +165,17 @@ export default {
beforeDestroy() {
this.clearFocusTrap()
this.clearEscapeStopPropagation()
},
methods: {
/**
* @return {HTMLElement|undefined}
*/
getPopoverContentElement() {
return this.$refs.popover?.$refs.popperContent?.$el
},
/**
* Add focus trap for accessibility.
*/
Expand All @@ -178,7 +186,7 @@ export default {
return
}
const el = this.$refs.popover?.$refs.popperContent?.$el
const el = this.getPopoverContentElement()
if (!el) {
return
Expand Down Expand Up @@ -210,6 +218,35 @@ export default {
}
},
/**
* Add stopPropagation for Escape.
* It prevents global Escape handling after closing popover.
*
* Manual event handling is used here instead of v-on because there is no direct access to the node.
* Alternative - wrap <template #popover> in a div wrapper.
*/
addEscapeStopPropagation() {
const el = this.getPopoverContentElement()
el?.addEventListener('keydown', this.stopKeydownEscapeHandler)
},
/**
* Remove stop Escape handler
*/
clearEscapeStopPropagation() {
const el = this.getPopoverContentElement()
el?.removeEventListener('keydown', this.stopKeydownEscapeHandler)
},
/**
* @param {KeyboardEvent} event - native keydown event
*/
stopKeydownEscapeHandler(event) {
if (event.type === 'keydown' && event.key === 'Escape') {
event.stopPropagation()
}
},
afterShow() {
/**
* Triggered after the tooltip was visually displayed.
Expand All @@ -221,6 +258,7 @@ export default {
this.$nextTick(() => {
this.$emit('after-show')
this.useFocusTrap()
this.addEscapeStopPropagation()
})
},
afterHide() {
Expand All @@ -229,6 +267,7 @@ export default {
*/
this.$emit('after-hide')
this.clearFocusTrap()
this.clearEscapeStopPropagation()
},
},
}
Expand Down
37 changes: 35 additions & 2 deletions src/components/NcSelect/NcSelect.vue
Expand Up @@ -749,7 +749,7 @@ export default {
/**
* Array of options
*
* @type {Array<string | number | { [key: string | number]: any }>}
* @type {Array<string | number | Record<string | number, any>>}
*
* @see https://vue-select.org/api/props.html#options
*/
Expand All @@ -768,6 +768,39 @@ export default {
default: '',
},
/**
* Customized component's response to keydown events while the search input has focus
*
* @see https://vue-select.org/guide/keydown.html#mapkeydown
*/
mapKeydown: {
type: Function,
/**
* Patched Vue-Select keydown events handlers map to stop Escape propagation in open select
*
* @param {Record<number, Function>} map - Mapped keyCode to handlers { <keyCode>:<callback> }
* @param {import('@nextcloud/vue-select').VueSelect} vm - VueSelect instance
* @return {Record<number, Function>} patched keydown event handlers
*/
default(map, vm) {
return {
...map,
/**
* Patched Escape handler to stop propagation from open select
*
* @param {KeyboardEvent} event - default keydown event handler
*/
27: (event) => {
if (vm.open) {
event.stopPropagation()
}
// Default VueSelect's handler
map[27](event)
},
}
},
},
/**
* When `appendToBody` is true, this sets the placement of the dropdown
*
Expand Down Expand Up @@ -804,7 +837,7 @@ export default {
*
* The `v-model` directive may be used for two-way data binding
*
* @type {string | number | { [key: string | number]: any } | Array<any>}
* @type {string | number | Record<string | number, any> | Array<any>}
*
* @see https://vue-select.org/api/props.html#value
*/
Expand Down

0 comments on commit 365cbad

Please sign in to comment.