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

fix(NcModal): temporary deactivate focus-traps on modal open #5783

Merged
merged 3 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 104 additions & 5 deletions src/components/NcAppNavigation/NcAppNavigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,105 @@
-->

<docs>
```vue
<template>
<div class="styleguide-nc-content">
<NcAppNavigation>
<template>
<div class="navigation__header">
<NcTextField :value.sync="searchValue" label="Search …" />
<NcActions>
<NcActionButton close-after-click @click="showModal = true">
<template #icon>
<IconCog />
</template>
App settings (close after click)
</NcActionButton>
<NcActionButton @click="showModal = true">
<template #icon>
<IconCog />
</template>
App settings (handle only click)
</NcActionButton>
</NcActions>
</div>
</template>
<template #list>
<NcAppNavigationItem v-for="item in items" :key="item" :name="item">
<template #icon>
<IconCheck :size="20" />
</template>
</NcAppNavigationItem>
</template>
<template #footer>
<div class="navigation__footer">
<NcButton wide @click="showModal = true">
<template #icon>
<IconCog />
</template>
App settings
</NcButton>
<NcModal v-if="showModal" name="Modal for focus-trap check" @close="showModal = false">
<div class="modal-content">
<h4>Focus-trap should be locked inside the modal</h4>
<NcTextField :value.sync="modalValue" label="Focus me" />
</div>
</NcModal>
</div>
</template>
</NcAppNavigation>
</div>
</template>

<script>
import IconCheck from 'vue-material-design-icons/Check'
import IconCog from 'vue-material-design-icons/Cog'

export default {
components: {
IconCheck,
IconCog,
},
provide() {
return {
'NcContent:setHasAppNavigation': () => {},
}
},
data() {
return {
items: Array.from({ length: 5 }, (v, i) => `Item ${i+1}`),
searchValue: '',
modalValue: '',
showModal: false,
}
},
}
</script>

<style scoped>
/* Mock NcContent */
.styleguide-nc-content {
position: relative;
height: 300px;
background-color: var(--color-background-plain);
overflow: hidden;
}

.navigation__header,
.navigation__footer {
display: flex;
align-items: center;
gap: 4px;
padding: 4px;
}

.modal-content {
height: 120px;
padding: 10px;
}
</style>
```

The navigation bar can be open and closed from anywhere in the app using the
nextcloud event bus.

Expand Down Expand Up @@ -159,7 +258,7 @@ export default {
* @param {boolean} [state] set the state instead of inverting the current one
*/
toggleNavigation(state) {
// Early return if alreay in that state
// Early return if already in that state
if (this.open === state) {
emit('navigation-toggled', {
open: this.open,
Expand Down Expand Up @@ -206,7 +305,7 @@ export default {
<style lang="scss">
.app-navigation,
.app-content {
/** Distance of the app naviation toggle and the first navigation item to the top edge of the app content container */
/** Distance of the app navigation toggle and the first navigation item to the top edge of the app content container */
--app-navigation-padding: #{$app-navigation-padding};
}
</style>
Expand All @@ -225,7 +324,7 @@ export default {
top: 0;
left: 0;
padding: 0px;
// Above appcontent
// Above NcAppContent
z-index: 1800;
height: 100%;
box-sizing: border-box;
Expand Down Expand Up @@ -283,14 +382,14 @@ export default {
}
}

// When on mobile, we make the navigation slide over the appcontent
// When on mobile, we make the navigation slide over the NcAppContent
@media only screen and (max-width: $breakpoint-mobile) {
.app-navigation {
position: absolute;
}
}

// Put the toggle behind appsidebar on small screens
// Put the toggle behind NcAppSidebar on small screens
@media only screen and (max-width: $breakpoint-small-mobile) {
.app-navigation {
z-index: 1400;
Expand Down
26 changes: 18 additions & 8 deletions src/components/NcModal/NcModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export default {
</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" />
<NcSelect input-id="pizza" :options="['Cheese', 'Tomatoes', 'Pineapples']" v-model="pizza" />
</div>
<div class="form-group">
<label for="emoji-trigger">Select your favorite emoji</label>
Expand Down Expand Up @@ -438,7 +438,7 @@ export default {
},

/**
* Close the modal if the user clicked outside of the modal
* Close the modal if the user clicked outside the modal
* Only relevant if `canClose` is set to true.
*/
closeOnClickOutside: {
Expand Down Expand Up @@ -529,6 +529,7 @@ export default {
slideshowTimeout: null,
iconSize: 24,
focusTrap: null,
externalFocusTrapStack: [],
randId: GenRandomId(),
internalShow: true,
}
Expand Down Expand Up @@ -701,8 +702,8 @@ export default {
}
if (arrowHandlers[event.key]) {
// Ignore arrow navigation, if there is a current focus outside the modal.
// For example, when the focus is in Sidebar or NcActions's items,
// arrow navigation should not be intercept by modal slider
// For example, when the focus is in Sidebar or NcActions' items,
// arrow navigation should not be intercepted by modal slider
if (document.activeElement && !this.$el.contains(document.activeElement)) {
return
}
Expand Down Expand Up @@ -793,12 +794,17 @@ export default {
allowOutsideClick: true,
fallbackFocus: contentContainer,
trapStack: getTrapStack(),
// Esc can be used without stop in content or additionalTrapElements where it should not deacxtivate modal's focus trap.
// Esc can be used without stop in content or additionalTrapElements where it should not deactivate modal's focus trap.
// Focus trap is deactivated on modal close anyway.
escapeDeactivates: false,
setReturnFocus: this.setReturnFocus,
}

// Deactivate other focus traps to unlock modal elements
this.externalFocusTrapStack = [...options.trapStack]
for (const trap of this.externalFocusTrapStack) {
trap.deactivate()
}
// Init focus trap
this.focusTrap = createFocusTrap([contentContainer, ...this.additionalTrapElements], options)
this.focusTrap.activate()
Expand All @@ -809,6 +815,10 @@ export default {
}
this.focusTrap?.deactivate()
this.focusTrap = null
for (const trap of this.externalFocusTrapStack) {
trap.activate()
}
this.externalFocusTrapStack = []
},

},
Expand Down Expand Up @@ -837,7 +847,7 @@ export default {
top: 0;
right: 0;
left: 0;
// prevent vue show to use display:none and reseting
// prevent vue show to use display:none and resetting
// the circle animation loop
display: flex !important;
align-items: center;
Expand Down Expand Up @@ -988,7 +998,7 @@ export default {
box-shadow: 0 0 40px rgba(0, 0, 0, .2);

&__close {
// Ensure the close button is always ontop of the content
// Ensure the close button is always on top of the content
z-index: 1;
position: absolute;
top: 4px;
Expand All @@ -998,7 +1008,7 @@ export default {
&__content {
width: 100%;
min-height: 52px; // At least the close button shall fit in
overflow: auto; // avoids unecessary hacks if the content should be bigger than the modal
overflow: auto; // avoids unnecessary hacks if the content should be bigger than the modal
}
}

Expand Down
Loading