Skip to content

Commit

Permalink
Merge pull request #5783 from nextcloud-libraries/fix/5385/focus-trap…
Browse files Browse the repository at this point in the history
…-mobile

fix(NcModal): temporary deactivate focus-traps on modal open
  • Loading branch information
susnux committed Jul 22, 2024
2 parents 7afe655 + 1f80896 commit 3fb2836
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 11 deletions.
105 changes: 102 additions & 3 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 @@ -229,7 +328,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 @@ -295,14 +394,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

0 comments on commit 3fb2836

Please sign in to comment.