Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stop propagation of keyboard navigation in a number of components #4501

Merged
merged 7 commits into from Sep 7, 2023
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